Your Azure SQL Backups Won’t Save You (Here’s Why)
“We have backups” is the IT equivalent of “thoughts and prayers.” Comforting words that mean nothing when disaster strikes. I’ve watched teams discover their Azure SQL Database backups expired just before an audit, or worse, during an actual outage. The default seven-day retention feels generous until you need data from day eight.
Compliance standards demand information backup in cloud environments, but no standard can enforce what most teams ignore: actually testing those backups. The gap between “we configured backups” and “we can restore our data” has ended careers and companies. This isn’t about checking compliance boxes. It’s about whether your business survives the next outage.
The shared responsibility trap
Cloud providers love talking about shared responsibility, and nowhere is this more critical than backup management. Microsoft handles the infrastructure: automated backups, storage redundancy, the underlying hardware. Your responsibility? Everything that actually matters for business continuity.
Azure SQL Database automatically creates backups. That’s where most teams stop thinking. They assume “automatic” means “sufficient.” But automatic backups with default settings are like automatic door locks that only work for a week. Technically functional, practically useless when you actually need protection.
Let me be clear about what “automatic” actually means in Azure SQL Database. Microsoft runs full backups weekly, differential backups every 12-24 hours, and transaction log backups every 5-10 minutes. That’s genuinely impressive infrastructure. But here’s the catch: all of it disappears after seven days unless you explicitly configure longer retention.
The fatal assumption: “Microsoft backs up my database, so I’m covered.” The reality: Microsoft backs up your database for seven days by default. After that, your compliance requirements, audit trail, and recovery capabilities vanish unless you’ve explicitly configured retention policies. I’ve seen this assumption cost teams their jobs, their compliance certifications, and in one memorable case, their entire business.
Fatal example: The compliance time bomb
Here’s what I see in most Azure SQL deployments:
// Fatal: Relying on defaults without understanding the consequences
resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
parent: sqlServer
name: 'production-database'
location: resourceGroup().location
sku: {
name: 'S2'
tier: 'Standard'
}
// No backup configuration = 7-day retention
// No long-term retention
// No geo-replication
}
Looks clean, right? Deploys successfully, runs in production, passes all the automated checks. But this configuration is a time bomb waiting for the first real test.
What’s actually wrong here:
Seven-day retention cliff: Your compliance officer asks for transaction data from last month. It doesn’t exist. That’s not a technical problem, that’s a compliance violation with financial penalties.
No long-term retention (LTR): Annual audits require historical data. “We don’t have it anymore” is never an acceptable answer.
Single region, single point of failure: When North Europe goes down (and it does happen), your database goes with it. Hope you didn’t promise 99.99% availability.
Untested recovery: Your Recovery Time Objective (RTO) and Recovery Point Objective (RPO) are whatever you wrote in the SLA document, not what your infrastructure actually delivers. Those are two very different numbers.
This configuration passes deployment validation. It fails the first real test: either a compliance audit asking for last year’s data or an actual disaster requiring regional failover. I’ve seen both happen, sometimes to the same team in the same quarter.
The recovery time reality
Before we fix the configuration, let’s talk about what you’re actually promising your stakeholders. RTO and RPO sound like consultant buzzwords until you’re explaining to the CEO why the recovery took six hours instead of the thirty minutes you promised.
Recovery Time Objective (RTO) answers: “How long until we’re back online?” Recovery Point Objective (RPO) answers: “How much data can we afford to lose?” These aren’t abstract metrics. They’re contractual commitments that determine whether your business survives an outage.
Azure SQL Database provides different RPO guarantees based on service tier:
- Basic/Standard/Premium: RPO ≤ 5 minutes (point-in-time restore from transaction log backups every 5-10 minutes)
- Hyperscale: RPO ≤ 10 minutes (snapshot-based backups)
- Business Critical with active geo-replication: RPO ≤ 5 seconds (synchronous replication to secondary)
The service tier determines your capabilities. A team choosing Standard tier for cost savings while contractually obligated to sub-minute RPO has created a compliance violation regardless of configuration quality.
Correct example: Backup architecture that actually works
Let’s fix this properly. I’ll show you the three resources that matter most for backup compliance: short-term retention, long-term retention, and geo-replication.
// Short-term retention: Point-in-time recovery for operational issues
resource backupShortTermRetention 'Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies@2023-05-01-preview' = {
parent: primaryDatabase
name: 'default'
properties: {
retentionDays: 35 // Maximum: 35 days (up from default 7)
diffBackupIntervalInHours: 12 // Differential backups every 12 hours
}
}
// Long-term retention: Compliance and historical recovery
resource backupLongTermRetention 'Microsoft.Sql/servers/databases/backupLongTermRetentionPolicies@2023-05-01-preview' = {
parent: primaryDatabase
name: 'default'
properties: {
weeklyRetention: 'P12W' // Keep weekly backups for 12 weeks
monthlyRetention: 'P12M' // Keep monthly backups for 12 months
yearlyRetention: 'P7Y' // Keep yearly backups for 7 years
weekOfYear: 1 // Use first week of year for yearly backup
}
}
// Geo-replication: Business continuity across regions
resource geoReplica 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
parent: secondarySqlServer // Server in different region (e.g., West Europe)
name: 'production-database'
location: 'westeurope'
properties: {
createMode: 'Secondary'
sourceDatabaseId: primaryDatabase.id
requestedBackupStorageRedundancy: 'Geo'
}
}
These three resources transform a seven-day liability into a multi-year safety net. Let me explain what each actually does:
Short-term retention covers your operational recovery scenarios. Someone drops a table at 2 AM? You can restore to any point in the last 35 days, down to the minute. The differential backup interval determines your worst-case RPO for point-in-time recovery. Twelve hours means you lose at most 12 hours of data if something catastrophic happens between differential backups.
Long-term retention satisfies your compliance requirements. Those P-prefixed values are ISO 8601 durations: P12W means 12 weeks, P12M means 12 months, P7Y means 7 years. Azure automatically creates and manages these backups. You just define how long to keep them.
Geo-replication handles regional disasters. When your primary region has an outage, your secondary database in another region is already synchronized and ready. The requestedBackupStorageRedundancy: 'Geo' ensures your backups are also stored across regions, not just your live data.
This configuration addresses the real compliance requirements:
- Demonstrable backup procedures: 35-day short-term plus 7-year long-term retention
- Automated backups: Full, differential, and transaction log backups with geo-redundant storage
- Business continuity: Documented RTO/RPO with automatic failover capabilities
- Redundancy: Zone-redundant primary, geo-replicated secondary
Point-in-time restore procedures
Configuration without tested procedures is wishful thinking. The Bicep templates deploy your backup policies, but can you actually restore data when it matters? Here’s the essential restore workflow:
#!/bin/bash
# Point-in-time restore for Azure SQL Database
set -euo pipefail
RESOURCE_GROUP="rg-production-sql"
SOURCE_SERVER="sql-primary-production"
SOURCE_DATABASE="production-database"
RESTORE_DATABASE="production-restore-$(date +%Y%m%d-%H%M%S)"
RESTORE_POINT="2026-01-15T14:30:00Z" # Must be within retention window
# Check earliest restorable point
EARLIEST=$(az sql db show -g "$RESOURCE_GROUP" -s "$SOURCE_SERVER" -n "$SOURCE_DATABASE" \
--query "earliestRestoreDate" -o tsv)
echo "Earliest restore point: $EARLIEST"
echo "Requested restore point: $RESTORE_POINT"
# Perform the restore
az sql db restore -g "$RESOURCE_GROUP" -s "$SOURCE_SERVER" \
-n "$RESTORE_DATABASE" --source-database "$SOURCE_DATABASE" \
--restore-point-in-time "$RESTORE_POINT" --service-objective "S2"
echo "Restore initiated. Monitor with:"
echo "az sql db show -g $RESOURCE_GROUP -s $SOURCE_SERVER -n $RESTORE_DATABASE --query status"
The critical line here is earliestRestoreDate. That tells you exactly how far back you can restore. If that date is more recent than you expected, your retention policy isn’t configured correctly. I’ve seen teams discover this gap at the worst possible moment: when they actually needed the data.
Automated restore validation
Manual restore procedures fail when they’re needed most, during actual disasters when stress and time pressure guarantee mistakes. I’ve seen runbooks that worked perfectly in testing fall apart during a 3 AM incident because someone missed a step or typed a parameter wrong.
Automated monthly validation proves your backups work before you need them. Here’s a simplified GitHub Actions workflow that tests the complete restore process:
# .github/workflows/backup-validation.yml
name: Monthly Backup Validation
on:
schedule:
- cron: '0 2 1-7 * 0' # First Sunday of each month
workflow_dispatch:
permissions:
id-token: write # Required for OIDC (OpenID Connect) authentication
contents: read
env:
RESOURCE_GROUP: 'rg-production-sql'
SERVER_NAME: 'sql-primary-production'
DATABASE_NAME: 'production-database'
jobs:
validate-backup:
runs-on: ubuntu-latest
steps:
- name: Azure login with OIDC
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Perform restore and validation
run: |
RESTORE_DB="validation-$(date +%Y%m%d-%H%M%S)"
RESTORE_POINT=$(date -u -d '5 minutes ago' +"%Y-%m-%dT%H:%M:%SZ")
START_TIME=$(date +%s)
# Restore to validation database
az sql db restore -g "$RESOURCE_GROUP" -s "$SERVER_NAME" \
-n "$RESTORE_DB" --source-database "$DATABASE_NAME" \
--restore-point-in-time "$RESTORE_POINT" --service-objective "S2"
# Wait and verify
while [[ $(az sql db show -g "$RESOURCE_GROUP" -s "$SERVER_NAME" \
-n "$RESTORE_DB" --query status -o tsv) != "Online" ]]; do
sleep 30
done
END_TIME=$(date +%s)
echo "Restore completed in $((END_TIME - START_TIME)) seconds"
# Cleanup
az sql db delete -g "$RESOURCE_GROUP" -s "$SERVER_NAME" \
-n "$RESTORE_DB" --yes --no-wait
The key insight here isn’t the YAML syntax, it’s the principle. Run this monthly, capture the metrics, and you have audit evidence that your backups actually work. When the compliance auditor asks “Can you prove your backups are restorable?” you have timestamped GitHub Actions artifacts showing successful restores for the past year.
The workflow uses OIDC (OpenID Connect) for Azure authentication instead of storing credentials as secrets. That’s important for compliance too: no long-lived credentials to rotate or leak.
Health monitoring for backup status
Monthly validation is great for audit evidence, but you also need continuous monitoring. The monthly test tells you backups worked last month. Health checks tell you they’re working right now.
Here’s a condensed ASP.NET Core health check that monitors the critical backup indicators:
public class AzureSqlBackupHealthCheck : IHealthCheck
{
private readonly IConfiguration _config;
private const int CriticalHours = 48; // Alert if no backup in 48h
public AzureSqlBackupHealthCheck(IConfiguration config) => _config = config;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken ct = default)
{
var armClient = new ArmClient(new DefaultAzureCredential());
var databaseId = SqlDatabaseResource.CreateResourceIdentifier(
_config["Azure:SubscriptionId"],
_config["Azure:ResourceGroup"],
_config["Azure:SqlServer"],
_config["Azure:Database"]);
var database = await armClient.GetSqlDatabaseResource(databaseId).GetAsync(ct);
var issues = new List<string>();
// Check 1: Can we restore? Is earliest restore point recent?
var earliestRestore = database.Value.Data.EarliestRestoreDate;
if (!earliestRestore.HasValue)
return HealthCheckResult.Unhealthy("No restore point available");
var backupAge = DateTimeOffset.UtcNow - earliestRestore.Value;
if (backupAge.TotalHours > CriticalHours)
return HealthCheckResult.Unhealthy(
$"No backups for {backupAge.TotalHours:F0} hours");
// Check 2: Is backup storage geo-redundant?
var redundancy = database.Value.Data.RequestedBackupStorageRedundancy;
if (redundancy != SqlBackupStorageRedundancy.Geo)
issues.Add($"Backup storage: {redundancy} (not geo-redundant)");
// Check 3: Is TDE (Transparent Data Encryption) enabled?
var tde = await database.Value.GetTransparentDataEncryptions()
.GetAsync("current", ct);
if (tde.Value.Data.State != SqlTransparentDataEncryptionState.Enabled)
return HealthCheckResult.Unhealthy("TDE not enabled");
return issues.Count > 0
? HealthCheckResult.Degraded(string.Join("; ", issues))
: HealthCheckResult.Healthy("Backup configuration verified");
}
}
This check answers the questions that matter: Can we restore right now? Are backups protected with geo-redundancy? Is the data encrypted? Wire this into your ASP.NET Core health endpoint, connect it to Azure Monitor, and you’ll know within minutes when something goes wrong. Not during the next audit or the next disaster.
The key property here is EarliestRestoreDate. That single value tells you exactly how far back you can recover. If it’s older than expected, something broke in your backup chain. I’ve seen this catch backup failures that would have gone unnoticed for weeks otherwise.
The compliance documentation gap
Teams focus on technical implementation and ignore the documentation that auditors actually request. Compliance standards require documented backup procedures, tested recovery processes, and demonstrable evidence that your retention policies actually work.
I’ve watched teams ace the technical implementation and then stumble during audits because they couldn’t produce evidence. The auditor doesn’t care about your elegant Bicep templates. They want to see proof that you tested recovery and it actually worked.
Your compliance documentation should include:
Configuration Evidence
- Short-term and long-term retention policies (what you configured)
- Geo-replication topology (where your data lives)
- Encryption validation (TDE status on primary and restored databases)
Recovery Procedures
- Point-in-time restore process with step-by-step instructions
- Failover group activation procedures (manual and automatic scenarios)
- Validation queries to verify data integrity after restore
Testing Evidence
- Monthly automated validation results (those GitHub Actions artifacts)
- Measured RTO/RPO from actual restore tests, not theoretical calculations, actual measurements
- Quarterly disaster recovery drill results with timestamps and outcomes
Monitoring Configuration
- Health check thresholds and alerting rules
- On-call procedures for backup degradation
- Escalation paths when restores fail
The technical implementation satisfies the engineering requirements. The documentation satisfies the compliance requirements. Skip either, and you’ve failed the audit regardless of how good the other half looks.
Lessons from production failures
I’ve investigated enough backup-related incidents to recognize the patterns:
Incident 1: The 7-day assumption Team assumed default seven-day retention satisfied audit requirements. Compliance officer requested transaction data from eight days prior. Data didn’t exist. Violation documented, compliance breach, financial penalty.
Incident 2: The untested restore Team configured geo-replication but never tested failover. Primary region outage triggered automatic failover. Application connection strings hardcoded to primary FQDN. Failover succeeded; application remained down for 4 hours during connection string deployment.
Incident 3: The encryption surprise Team enabled TDE on new databases but missed enabling on restored databases. Compliance scan detected unencrypted backups. Six months of backups failed compliance requirements.
These aren’t hypothetical scenarios. They’re production failures that wouldn’t have occurred with proper testing and validation.
Final thoughts: Backups without verification are just expensive storage
Azure SQL Database automatic backups are convenient until you realize convenience doesn’t equal reliability. Compliance requires verifiable data recovery capability, not backup configuration, but actual proof that you can restore your data when it matters.
The configuration I’ve demonstrated addresses the real requirements: proper retention that survives audit timelines, geo-redundancy that survives regional outages, encryption that survives compliance scans, and automated validation that survives the “prove it works” conversation.
Your backup strategy is only as reliable as your most recent successful restore. If you haven’t tested recovery in the past 30 days, you don’t have backups. You have optimistic assumptions stored in Azure. The distinction becomes painfully clear when you actually need the data.
Here’s the uncomfortable truth: most teams discover their backup gaps during audits or disasters, not during normal operations. The configurations and validations in this article exist precisely to move that discovery earlier. To a scheduled GitHub Action at 2 AM on a Sunday, not to a panicked 3 AM incident response when the primary region is down.
Configure proper retention. Implement geo-replication. Automate your validation testing. Monitor continuously. Document everything. That’s not paranoia. That’s the difference between “we have backups” and “we can restore our data.”

Comments