Retry Logic for Azure SQL in .NET: Handling Transient Errors the Right Way
In the cloud, short interruptions are normal. Azure SQL Database occasionally moves a database to another node for maintenance, scaling or load balancing, and during those few seconds connections drop or new ones are refused. A well-built application shrugs these off with a short retry. A badly built one shows users an error page.

Transient vs permanent errors
Only transient errors should be retried: ones that will probably succeed if you try again in a moment. Examples commonly treated as transient for Azure SQL include:
- 40613: database is not currently available (often during a failover).
- 40197: the service encountered an error processing your request.
- 40501: the service is currently busy.
- 49918, 49919, 49920: cannot process request because of too many operations or requests.
- Connection-level failures such as timeouts while connecting or a forcibly closed connection.
Errors like a syntax error, a missing table, a permission denied or a constraint violation are not transient. Retrying them only delays the failure. The libraries below already know the standard transient list, which is one good reason to use them rather than hand-rolling your own.
Back off exponentially, with jitter
Retrying instantly in a tight loop makes things worse during an outage. Wait a little longer each time (for example 1, 2, 4, 8 seconds), add some randomness so thousands of clients do not retry at the same moment, and give up after a sensible total time.
EF Core: turn on the built-in strategy
services.AddDbContext<PayrollContext>(options =>
options.UseSqlServer(connectionString, sql =>
sql.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null)));EF Core then retries queries and SaveChanges automatically on transient SQL errors.
Manual transactions need an execution strategy
With retries enabled, a transaction you open yourself must be wrapped so the whole unit is retried together; otherwise EF Core throws an exception telling you to do so:
var strategy = db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
await using var tx = await db.Database.BeginTransactionAsync();
db.PayrollRuns.Add(run);
await db.SaveChangesAsync();
await db.Database.ExecuteSqlRawAsync("EXEC dbo.PostPayrollRun @p0", run.Id);
await tx.CommitAsync();
});ADO.NET and Dapper: SqlClient retry logic
Microsoft.Data.SqlClient includes configurable retry logic that you can attach to a connection or command:
var options = new SqlRetryLogicOption
{
NumberOfTries = 5,
DeltaTime = TimeSpan.FromSeconds(1),
MaxTimeInterval = TimeSpan.FromSeconds(20)
};
var provider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(options);
await using var conn = new SqlConnection(connectionString);
conn.RetryLogicProvider = provider; // retries opening the connection
await conn.OpenAsync();You can also set RetryLogicProvider on individual SqlCommand objects. Alternatively, many teams use a resilience library such as Polly to wrap data calls with the same retry policy as their HTTP calls.
Connection string settings
For idle connections that break, SqlClient can reconnect automatically using ConnectRetryCount and ConnectRetryInterval. A slightly longer Connect Timeout (such as 30 seconds) also helps during failovers:
Server=tcp:myserver.database.windows.net,1433;Database=Payroll;
Connect Timeout=30;ConnectRetryCount=3;ConnectRetryInterval=10;Only retry what is safe to repeat
If a timeout happens after SQL Server committed your INSERT but before your app received the reply, a blind retry inserts a duplicate. Make writes idempotent: use unique keys, check-then-insert patterns, or a client-generated request ID so the second attempt is recognised and ignored.
Quick checklist
- Retry only transient errors, using a library that knows them.
- Exponential backoff with jitter, capped total time.
- EF Core:
EnableRetryOnFailure, and execution strategies for manual transactions. - ADO.NET/Dapper: SqlClient retry logic or Polly.
- Make writes idempotent and log every retry so you can see patterns.
Related reading:
Comments
Post a Comment