Cloud applications frequently encounter transient database failures. A connection can temporarily fail because of network interruptions, database failover, throttling, resource pressure, or other infrastructure conditions.
For an Azure SQL application, immediately treating every database exception as a permanent failure can make an otherwise healthy application appear unreliable.
Modern .NET applications can implement SQL-specific retry behavior through Microsoft.Data.SqlClient, allowing transient failures to be retried without introducing a separate general-purpose resilience library for database operations.
The important part is not simply enabling retries. A production implementation must define which operations are safe to retry, how many attempts are allowed, how delays are calculated, and how failures are ultimately reported.
Why Azure SQL Applications Need Retry Logic
Consider a web API executing a database query:
API Request
|
v
Azure SQL
|
X
Transient FailureWithout retry handling:
Request
↓
SQL Failure
↓
HTTP 500With appropriate retry behavior:
Request
↓
SQL Operation
↓
Transient Failure
↓
Wait
↓
Retry
↓
SuccessThis can allow short-lived infrastructure problems to recover without immediately failing the application request.
However, retrying is not appropriate for every database error.
What Is a Transient Failure?
A transient failure is generally a temporary condition where repeating the operation later may succeed.
Examples can include:
Temporary connectivity problems
Database failover
Resource throttling
Temporary service availability issues
Network interruptions
A permanent failure is different.
For example:
Invalid SQL
Invalid table
Invalid credentials
Constraint violation
Invalid application dataRetrying these conditions usually does not solve the underlying problem.
The retry mechanism therefore needs to distinguish retryable conditions from non-retryable ones.
Microsoft.Data.SqlClient Retry Logic
Microsoft.Data.SqlClient provides retry-related functionality that can be configured for SQL Server and Azure SQL workloads.
A simplified configuration concept looks like:
using Microsoft.Data.SqlClient;
var connectionString =
builder.Configuration.GetConnectionString("Database");
var builder = new SqlConnectionStringBuilder(
connectionString)
{
ConnectRetryCount = 5,
ConnectRetryInterval = 2
};
builder.Services.AddSingleton(
new SqlConnectionStringBuilder(
builder.ConnectionString));The exact configuration should be aligned with the version of Microsoft.Data.SqlClient used by the application.
Also note that connection retry settings primarily address connection establishment and related transient connectivity scenarios. They should not be confused with automatically retrying every SQL command.
Connection Retry vs Command Retry
This distinction is important.
There are two different layers:
Application
|
v
Open SQL Connection
|
+---- Connection Retry
|
v
Execute SQL Command
|
+---- Command RetryA connection can succeed while a subsequent command encounters a transient failure.
Therefore, configuring connection retry does not automatically mean every database operation has a complete retry policy.
Using SqlConnection
A typical repository method can remain straightforward:
using Microsoft.Data.SqlClient;
public async Task<Customer?> GetCustomerAsync(
int customerId,
CancellationToken cancellationToken)
{
await using var connection =
new SqlConnection(connectionString);
await connection.OpenAsync(
cancellationToken);
const string sql = """
SELECT Id, Name, Email
FROM Customers
WHERE Id = @id
""";
await using var command =
new SqlCommand(sql, connection);
command.Parameters.AddWithValue(
"@id",
customerId);
await using var reader =
await command.ExecuteReaderAsync(
cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
{
return null;
}
return new Customer(
reader.GetInt32(0),
reader.GetString(1),
reader.GetString(2));
}The query uses a parameter rather than concatenating user input into SQL.
Retry logic should not be used as a substitute for safe SQL construction.
Configuring Retry Logic
Microsoft.Data.SqlClient also provides retry-policy APIs for more granular scenarios.
A retry policy can define:
Retryable Errors
Maximum Attempts
Delay
Backoff Strategy
Transient Error DetectionConceptually:
var retryPolicy =
new SqlRetryLogicOption
{
NumberOfTries = 5,
DeltaTime = TimeSpan.FromSeconds(2),
MaxTimeInterval =
TimeSpan.FromSeconds(30)
};The exact API surface depends on the installed Microsoft.Data.SqlClient version, so developers should verify the supported members for their package version.
Exponential Backoff
A retry system should avoid sending all retry requests immediately.
Consider:
Attempt 1 → Immediate
Attempt 2 → Short delay
Attempt 3 → Longer delay
Attempt 4 → Longer delayThis is commonly called exponential backoff.
A simplified implementation is:
private static TimeSpan GetDelay(
int attempt)
{
var seconds = Math.Pow(2, attempt);
return TimeSpan.FromSeconds(
Math.Min(seconds, 30));
}For production systems, a retry strategy should also consider jitter so that many application instances do not retry at exactly the same moment.
Why Jitter Matters
Imagine 500 API instances all experience the same transient database event.
If every instance retries after exactly five seconds:
500 Requests
|
v
Failure
|
v
Exactly 5 seconds
|
v
500 RetriesThat can create another load spike.
Jitter introduces controlled randomness into the delay:
Request A → 2.1s
Request B → 2.8s
Request C → 3.4s
Request D → 2.5sThe exact strategy should be chosen according to the application's workload and resilience requirements.
Retrying Commands Safely
Retrying a SELECT query is usually easier to reason about than retrying a state-changing operation.
Consider:
UPDATE Accounts
SET Balance = Balance - @amount
WHERE Id = @accountId;If the client loses its connection after the database processes the update but before the client receives the response, the application may not know whether the operation succeeded.
Automatically executing the update again could produce unintended behavior.
This creates the classic ambiguity:
Client
|
| UPDATE
v
Database
|
| Operation succeeds
X
Network failure
|
v
Client thinks it failedRetrying without considering idempotency can therefore be dangerous.
Idempotency for Write Operations
For critical writes, use an idempotency strategy where appropriate.
For example, an application can associate a unique operation ID:
public sealed record PaymentOperation(
Guid OperationId,
int AccountId,
decimal Amount);The database can record the operation ID.
Before applying the operation again, the application can determine whether the operation has already been processed.
Conceptually:
Operation ID
|
v
Already Processed?
/ \
Yes No
| |
Return Execute
Result OperationThis is much safer than blindly retrying every write.
Retry Budget
Retries should have a finite budget.
For example:
const int maxAttempts = 3;Then:
for (var attempt = 1;
attempt <= maxAttempts;
attempt++)
{
try
{
return await ExecuteAsync(
cancellationToken);
}
catch (SqlException) when (
attempt < maxAttempts)
{
await Task.Delay(
GetDelay(attempt),
cancellationToken);
}
}
throw new InvalidOperationException(
"The database operation could not be completed.");This example intentionally simplifies transient-error classification. Production code should retry only errors that the SQL client identifies as appropriate for the configured policy.
Respect Cancellation
Retry delays should respect the request's cancellation token.
await Task.Delay(
delay,
cancellationToken);This is particularly important in ASP.NET Core.
If the user disconnects while the server is waiting to retry a database operation, the application should not continue unnecessary work indefinitely.
Do Not Retry Everything
A dangerous pattern is:
catch (Exception)
{
await Task.Delay(1000);
// Retry
}This can retry:
Programming errors
Invalid SQL
Authentication failures
Data constraint violations
Serialization problems
Cancellation
Unexpected bugs
Retry only conditions that are actually transient.
Logging Retry Attempts
Retry activity should be observable.
For example:
logger.LogWarning(
"Transient SQL failure. Retry attempt {Attempt}.",
attempt);Useful telemetry can include:
Operation
Attempt Number
Elapsed Time
Failure Classification
Final OutcomeAvoid logging:
Connection strings containing secrets
Passwords
Access tokens
Sensitive query parameters
Confidential customer data
Metrics to Monitor
A production application should monitor retry behavior.
Useful metrics include:
| Metric | Why it matters |
|---|---|
| Retry count | Shows transient failure frequency |
| Retry success rate | Shows whether retries are effective |
| Final failure rate | Measures unresolved failures |
| Retry delay | Shows added latency |
| Operation type | Helps identify problematic workloads |
| Database response time | Helps identify resource pressure |
A rising retry rate can be an operational warning even when requests are still succeeding.
For example:
Requests: Stable
Success Rate: Stable
SQL Retries: IncreasingThis may indicate an underlying infrastructure or database problem that should be investigated.
Azure SQL and Connection Pooling
ADO.NET connection pooling means applications typically reuse physical connections rather than establishing a new physical connection for every logical database operation.
Therefore, application code should generally create and dispose SqlConnection objects normally:
await using var connection =
new SqlConnection(connectionString);
await connection.OpenAsync(
cancellationToken);Disposing the connection returns it to the pool when appropriate.
Retry logic should be designed with this behavior in mind rather than trying to maintain long-lived connection objects manually.
Comparing SqlClient Retry Logic and Polly
Both approaches can be useful, but they operate at different abstraction levels.
| Capability | SqlClient Retry Logic | Polly |
|---|---|---|
| SQL-specific behavior | Strong | Requires configuration |
| General HTTP resilience | No | Yes |
| Database operation awareness | Stronger | Generic |
| Retry policies | Yes | Yes |
| Broader application resilience | Limited | Strong |
| Additional dependency | SQL client feature | Separate library |
| Best fit | SQL-specific resilience | Cross-service resilience |
If the requirement is specifically SQL connectivity and SQL operation resilience, Microsoft.Data.SqlClient can provide a focused solution.
If the application needs a broader resilience strategy covering HTTP, messaging, databases, and other dependencies, a general resilience framework may be more appropriate.
The two approaches should not be evaluated only on the number of features. The correct choice depends on the application's architecture.
Common Mistakes
Retrying Every SQL Exception
Not every SQL error is transient.
Retrying Non-Idempotent Writes
A successful database operation followed by a network failure can create ambiguous results.
Using Fixed Delays Everywhere
Fixed retry intervals can cause synchronized retry storms.
Ignoring Cancellation
Long retry sequences can continue after the original request is no longer relevant.
Logging Sensitive Data
Retry diagnostics should never expose credentials or confidential database information.
Adding Retries at Multiple Layers
For example:
HTTP Retry
↓
Service Retry
↓
Repository Retry
↓
SQL Client RetryNested retries can multiply the number of attempts unexpectedly.
Define clear ownership of retry behavior.
Best Practices
Retry only transient failures.
Keep retry counts finite.
Use backoff for repeated attempts.
Consider jitter in distributed workloads.
Respect cancellation tokens.
Treat write operations differently from reads.
Design critical writes for idempotency where appropriate.
Log retry attempts without exposing sensitive data.
Monitor retry frequency and final failures.
Avoid overlapping retry policies at multiple application layers.
Keep database resilience separate from business logic.
Test failure scenarios before deploying retry behavior.
Advantages and Disadvantages
Advantages
Handles temporary SQL connectivity problems
Reduces failures caused by short-lived infrastructure conditions
Integrates with SQL client behavior
Can reduce the need for custom database retry code
Provides a focused approach for SQL-specific resilience
Works naturally with asynchronous .NET database operations
Disadvantages
Retries add latency
Incorrect retries can duplicate operations
Not every database failure is recoverable
Excessive retries can increase database pressure
Distributed applications require careful retry coordination
SQL-specific retry logic does not solve unrelated dependency failures
Troubleshooting Retry Problems
If retries are not behaving as expected:
Confirm the installed
Microsoft.Data.SqlClientversion.Verify that the retry configuration is actually being applied.
Identify the exact SQL exception.
Determine whether the failure is transient.
Check the number of retry attempts.
Review retry delays.
Check whether another resilience layer is also retrying.
Review database and application telemetry.
Check connection-pool behavior.
Test the operation under controlled transient failures.
If requests suddenly become much slower after enabling retries, inspect the number of attempts and cumulative delay before changing unrelated application code.
A Practical Architecture
A clean application can keep retry responsibility close to the database infrastructure:
ASP.NET Core API
|
v
Application Service
|
v
Repository
|
v
Microsoft.Data.SqlClient
|
+---- Connection Retry
+---- SQL Retry Policy
|
v
Azure SQLThe application service should not need to know how SQL transient failures are handled.
This keeps resilience implementation details inside the infrastructure layer.
Conclusion
Azure SQL applications need to assume that temporary connectivity and infrastructure failures can occur. Retry logic can make applications more resilient, but only when it is applied carefully.
Microsoft.Data.SqlClient provides SQL-focused retry capabilities that can be useful when the primary requirement is database resilience without introducing a separate general-purpose resilience dependency.
The most important consideration is not the number of retries. It is what is being retried and whether repeating that operation is safe.
Use finite retries, appropriate backoff, cancellation support, secure logging, and explicit handling for idempotent versus non-idempotent operations. Monitor retry frequency as an operational signal rather than treating successful retries as invisible events.
With these principles, .NET applications can handle transient Azure SQL failures more gracefully while avoiding the common problems caused by uncontrolled or overly broad retry strategies.

Join the conversation! Your thoughts help the community grow.