Modern .NET applications often depend on SQL Server for critical operations such as authentication, transactions, reporting, and business workflows. Even when an application and database are correctly configured, temporary failures can still occur because of network interruptions, connection pool pressure, database failovers, or transient infrastructure issues.
A retry strategy can help an application recover from these short-lived failures without immediately returning an error to the user.
The Microsoft.Data.SqlClient library provides built-in retry capabilities that can be configured for SQL Server connections and commands. This article explains how retry logic works, how to configure it in a .NET application, when it should be used, and what mistakes to avoid in production.
What Is Retry Logic in Microsoft.Data.SqlClient?
Retry logic allows Microsoft.Data.SqlClient to automatically retry certain operations when a transient SQL Server failure occurs.
Instead of following this pattern:
Application
↓
SQL Server
↓
Temporary failure
↓
Application returns error
a retry-enabled application can follow:
Application
↓
SQL Server
↓
Temporary failure
↓
Wait
↓
Retry
↓
SQL Server
↓
Success
This is particularly useful for cloud-hosted databases and distributed applications where temporary connectivity problems can occur.
Retry logic is not intended to hide permanent failures. For example, an invalid SQL statement, authentication failure, or missing table generally cannot be fixed by repeatedly executing the same operation.
Why Transient SQL Failures Happen
Transient failures are temporary conditions where the same operation may succeed if attempted again after a short delay.
Common examples include:
| Failure scenario | Retry potentially useful? | Reason |
|---|
| Temporary network interruption | Yes | Connectivity may recover |
| SQL Server failover | Yes | New connection may become available |
| Temporary resource pressure | Yes | Database resources may become available |
| Connection timeout during transient infrastructure issue | Sometimes | A later attempt may succeed |
| Invalid SQL syntax | No | The command itself is incorrect |
| Invalid credentials | No | Retrying does not fix authentication |
| Missing database/table | No | Requires configuration or code changes |
| Constraint violation | Usually no | The application data needs correction |
The important distinction is between transient and non-transient failures.
Configuring Retry Logic with Microsoft.Data.SqlClient
The retry functionality is available through Microsoft.Data.SqlClient.
A basic connection can be created like this:
using Microsoft.Data.SqlClient;
var connectionString =
"Server=localhost;Database=SalesDb;Integrated Security=True;TrustServerCertificate=True;";
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
To add retry behavior, configure a retry provider and assign it to the connection.
For example:
using Microsoft.Data.SqlClient;
var connectionString =
"Server=localhost;Database=SalesDb;Integrated Security=True;TrustServerCertificate=True;";
var retryProvider = SqlConfigurableRetryFactory.CreateExponentialRetryProvider(
retryCount: 5,
maxTimeInterval: TimeSpan.FromSeconds(10),
deltaTime: TimeSpan.FromSeconds(1));
using var connection = new SqlConnection(connectionString)
{
RetryLogicProvider = retryProvider
};
await connection.OpenAsync();
Here, the retry provider controls how the client responds when a retryable transient error occurs.
Understanding the Retry Parameters
The following values are important:
SqlConfigurableRetryFactory.CreateExponentialRetryProvider(
retryCount: 5,
maxTimeInterval: TimeSpan.FromSeconds(10),
deltaTime: TimeSpan.FromSeconds(1));
retryCount specifies how many retry attempts can be made.
maxTimeInterval limits the maximum delay between retry attempts.
deltaTime controls the delay growth used by the exponential retry strategy.
The goal is to avoid immediately sending the same failed request repeatedly.
Exponential Backoff and Why It Matters
A fixed retry strategy might look like this:
Attempt 1 → Fail
Wait 1 second
Attempt 2 → Fail
Wait 1 second
Attempt 3 → Fail
This can put unnecessary pressure on an already struggling database.
Exponential backoff gradually increases the waiting period:
Attempt 1 → Fail
Wait
Attempt 2 → Fail
Wait longer
Attempt 3 → Fail
Wait even longer
Attempt 4 → Success
This gives the underlying infrastructure time to recover.
In distributed systems, exponential backoff is generally more appropriate than aggressively retrying the same request without delay.
Using Retry Logic with ASP.NET Core
In an ASP.NET Core application, it is common to centralize database configuration instead of creating connections throughout the application.
For example:
using Microsoft.Data.SqlClient;
var builder = WebApplication.CreateBuilder(args);
var connectionString =
builder.Configuration.GetConnectionString("DefaultConnection");
var retryProvider =
SqlConfigurableRetryFactory.CreateExponentialRetryProvider(
retryCount: 5,
maxTimeInterval: TimeSpan.FromSeconds(10),
deltaTime: TimeSpan.FromSeconds(1));
builder.Services.AddScoped<SqlConnection>(_ =>
{
return new SqlConnection(connectionString)
{
RetryLogicProvider = retryProvider
};
});
var app = builder.Build();
app.MapGet("/products", async (SqlConnection connection) =>
{
await connection.OpenAsync();
using var command = new SqlCommand(
"SELECT Id, Name, Price FROM Products",
connection);
using var reader = await command.ExecuteReaderAsync();
var products = new List<object>();
while (await reader.ReadAsync())
{
products.Add(new
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Price = reader.GetDecimal(2)
});
}
return Results.Ok(products);
});
app.Run();
The main benefit of this approach is consistency. Database connections created through the configured dependency-injection registration receive the same retry configuration.
For larger applications, you would typically keep database access inside a repository or data-access service rather than placing SQL directly inside an endpoint.
Connection Retry vs Command Retry
One important aspect of Microsoft.Data.SqlClient retry logic is understanding what is actually being retried.
A database operation can involve:
Opening a connection
Executing a command
Reading results
Committing a transaction
Retry behavior must therefore be considered carefully, particularly for commands that modify data.
A read operation such as:
SELECT Id, Name
FROM Products
WHERE Id = @id
is generally easier to retry safely.
A write operation such as:
UPDATE Accounts
SET Balance = Balance - @amount
WHERE Id = @id
requires more consideration.
If the server processed the update but the client lost the connection before receiving the response, blindly retrying could potentially execute the business operation again.
The key issue is idempotency.
Designing Retry-Safe Database Operations
Before enabling aggressive retry behavior for write operations, determine whether repeating the operation produces the same intended result.
For example, this operation is not naturally idempotent:
UPDATE Accounts
SET Balance = Balance - 100
WHERE Id = 10;
Running it twice subtracts the amount twice.
A better design can use an operation identifier or another mechanism that allows the application to recognize an already-processed request.
For example:
INSERT INTO PaymentOperations
(
OperationId,
AccountId,
Amount
)
VALUES
(
@OperationId,
@AccountId,
@Amount
);
A unique constraint on OperationId can help prevent duplicate processing.
The exact implementation depends on the application's business requirements, but the principle is important:
Retrying infrastructure operations is different from safely retrying business operations.
Retry Logic Compared with Polly
.NET applications have traditionally used libraries such as Polly for resilience strategies.
Microsoft.Data.SqlClient retry logic and Polly can solve related but different problems.
| Capability | Microsoft.Data.SqlClient | Polly |
|---|
| SQL Server-specific retry | Excellent | Requires configuration |
| Database-aware transient handling | Built in | Application-defined |
| HTTP retry | No | Yes |
| Circuit breaker | No | Yes |
| Timeout policies | Limited to client behavior | Yes |
| Broader application resilience | Limited | Strong |
| SQL-specific configuration | Simple | More custom |
If your requirement is specifically SQL Server client retry behavior, the built-in Microsoft.Data.SqlClient functionality can be a straightforward choice.
If you need a broader resilience pipeline covering HTTP calls, caching, messaging, database operations, and other dependencies, an application-level resilience library may be more appropriate.
The two approaches should not be added blindly on top of each other. Layering multiple retry policies can result in unexpectedly large numbers of attempts.
Best Practices for Production
1. Keep Retry Counts Reasonable
More retries do not automatically mean better reliability.
A request that fails repeatedly can hold application resources and increase latency.
Start with conservative values and adjust them based on the application's requirements.
2. Use Exponential Backoff
Avoid immediate retry loops.
An exponential strategy gives SQL Server and the underlying infrastructure time to recover.
3. Do Not Retry Every Exception
Retry logic should only apply to failures that are actually transient.
An authentication problem will not normally be solved by five more login attempts.
4. Consider Operation Idempotency
Pay particular attention to:
A retry can turn a temporary connection problem into a duplicate business operation if the design is not idempotent.
5. Monitor Retries
Retries can hide problems if they are not observable.
Track useful information such as:
A system that succeeds only after multiple retries may appear healthy to users while the underlying database infrastructure is experiencing problems.
Common Mistakes
Setting an Excessive Retry Count
A configuration such as:
retryCount: 50
may keep requests alive for an unnecessarily long time.
Retry policies should be based on the application's acceptable latency and failure-recovery requirements.
Retrying Permanent Errors
Retrying invalid SQL repeatedly wastes resources.
Fix the underlying problem instead of increasing the retry count.
Adding Multiple Retry Layers
For example:
HTTP retry
↓
Service retry
↓
Repository retry
↓
SqlClient retry
A single failed database operation can potentially trigger many actual database requests.
Define clear ownership of retry behavior.
Ignoring Transactions
Transactions require special attention because a connection failure does not always tell the client whether the server completed the transaction.
Do not assume that an unsuccessful client response means that the database definitely rolled back the operation.
Troubleshooting Retry Behavior
When retry logic does not behave as expected, check the following:
Verify that the application is using Microsoft.Data.SqlClient, not an unrelated SQL client package.
Confirm that the retry provider is assigned to the connection or command configuration being used.
Check the actual exception and SQL error information.
Verify that the failure is transient and eligible for retry.
Check application logs for repeated attempts.
Review connection and command timeout settings.
Check SQL Server health, networking, resource utilization, and failover events.
Make sure another resilience layer is not already retrying the same operation.
Logging should make it possible to distinguish an operation that succeeded immediately from one that succeeded only after several retries.
Advantages and Disadvantages
Advantages
Built specifically for SQL Server client operations
Reduces failures caused by temporary connectivity problems
Supports configurable retry behavior
Exponential retry strategies reduce aggressive retry traffic
Can simplify SQL-specific resilience configuration
Works naturally with Microsoft.Data.SqlClient
Disadvantages
Does not solve permanent database failures
Excessive retries can increase application latency
Retrying write operations can introduce duplicate business operations
It does not replace broader application resilience patterns
Multiple retry layers can create unexpectedly high request counts
Conclusion
Transient database failures are a normal consideration for distributed .NET applications, particularly when applications depend on remote or cloud-hosted SQL Server environments.
Microsoft.Data.SqlClient provides a practical way to introduce SQL-aware retry behavior without implementing every retry mechanism manually. Exponential backoff, sensible retry limits, correct transient-error handling, and good observability are the key pieces of a reliable implementation.
However, retry logic should not be treated as a universal solution. The most important production consideration is understanding what is being retried and whether repeating that operation is safe.
For read-heavy workloads, carefully configured SqlClient retry logic can provide useful resilience against temporary database connectivity problems. For write-heavy or transactional workloads, combine retry configuration with idempotent application design, transaction awareness, monitoring, and an overall resilience strategy.