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 Failure

Without retry handling:

Request
   ↓
SQL Failure
   ↓
HTTP 500

With appropriate retry behavior:

Request
   ↓
SQL Operation
   ↓
Transient Failure
   ↓
Wait
   ↓
Retry
   ↓
Success

This 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:

A permanent failure is different.

For example:

Invalid SQL
Invalid table
Invalid credentials
Constraint violation
Invalid application data

Retrying 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 Retry

A 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 Detection

Conceptually:

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 delay

This 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 Retries

That 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.5s

The 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 failed

Retrying 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       Operation

This 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:

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 Outcome

Avoid logging:

Metrics to Monitor

A production application should monitor retry behavior.

Useful metrics include:

MetricWhy it matters
Retry countShows transient failure frequency
Retry success rateShows whether retries are effective
Final failure rateMeasures unresolved failures
Retry delayShows added latency
Operation typeHelps identify problematic workloads
Database response timeHelps 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:    Increasing

This 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.

CapabilitySqlClient Retry LogicPolly
SQL-specific behaviorStrongRequires configuration
General HTTP resilienceNoYes
Database operation awarenessStrongerGeneric
Retry policiesYesYes
Broader application resilienceLimitedStrong
Additional dependencySQL client featureSeparate library
Best fitSQL-specific resilienceCross-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 Retry

Nested retries can multiply the number of attempts unexpectedly.

Define clear ownership of retry behavior.

Best Practices

  1. Retry only transient failures.

  2. Keep retry counts finite.

  3. Use backoff for repeated attempts.

  4. Consider jitter in distributed workloads.

  5. Respect cancellation tokens.

  6. Treat write operations differently from reads.

  7. Design critical writes for idempotency where appropriate.

  8. Log retry attempts without exposing sensitive data.

  9. Monitor retry frequency and final failures.

  10. Avoid overlapping retry policies at multiple application layers.

  11. Keep database resilience separate from business logic.

  12. Test failure scenarios before deploying retry behavior.

Advantages and Disadvantages

Advantages

Disadvantages

Troubleshooting Retry Problems

If retries are not behaving as expected:

  1. Confirm the installed Microsoft.Data.SqlClient version.

  2. Verify that the retry configuration is actually being applied.

  3. Identify the exact SQL exception.

  4. Determine whether the failure is transient.

  5. Check the number of retry attempts.

  6. Review retry delays.

  7. Check whether another resilience layer is also retrying.

  8. Review database and application telemetry.

  9. Check connection-pool behavior.

  10. 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 SQL

The 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.