Use Polly in .NET Worker Service for Reliable Database Calls

When building background processing applications in .NET, Worker Services are commonly used to handle scheduled jobs, message queue processing, data synchronization, and long-running tasks. These services often interact with databases continuously, making reliability and fault tolerance critical for production environments.

However, database operations are not always stable. Temporary network interruptions, connection timeouts, deadlocks, or transient SQL failures can cause your Worker Service to fail unexpectedly. Without proper handling, these issues may lead to application crashes, data inconsistency, or missed processing tasks.

This is where Polly becomes extremely useful. Polly is a powerful .NET resilience and transient-fault-handling library that helps developers implement retry policies, circuit breakers, timeout handling, and fallback strategies with minimal code changes.

In this article, we will learn how to use Polly in a .NET Core Worker Service to make database calls more reliable and resilient. We will implement retry policies for transient database failures and understand how Polly improves the stability of background services in real-world applications.

Use Polly in .NET Worker Service for Reliable Database Calls

Getting Started

Polly is primarily introduced as a general resilience library, and the most common examples are around HttpClient and APIs. But Polly is not limited to HTTP calls. It works with any operation in .NET, including:

  • database calls
  • file I/O
  • message queues
  • gRPC
  • Redis
  • background jobs
  • third-party SDKs
Polly for .NET is commonly used for database calls, especially to handle transient failures such as:
  • Temporary network interruptions
  • SQL deadlocks
  • Connection timeouts
  • Azure SQL throttling
  • Failover events
  • Brief database overloads
Typical resilience strategies for databases include:
  1. Retry: The Retry policy automatically retries failed database calls when transient failures occur.
      Benefits:
    • Handles temporary network failures
    • Reduces random SQL timeout issues
    • Improves application resilience
  2. Timeout: The Timeout policy prevents database operations from running indefinitely.
      Benefits:
    • Prevents hanging database calls
    • Protects thread resources
    • Improves system responsiveness
  3. Circuit breaker: The Circuit Breaker temporarily stops database calls after repeated failures.
      Benefits:
    • Prevents overwhelming the database
    • Avoids repeated failed requests
    • Gives downstream systems time to recover
Using Polly around database operations is a very standard and valid pattern when:
  • you use raw ADO.NET / Dapper
  • you need custom retry logic
  • you want circuit breakers
  • you want centralized resilience handling

Implementing Polly Strategies in .NET Worker Service with ADO.NET

In this section, we will implement Retry, Timeout, and Circuit Breaker strategies using Polly while calling a database through ADO.NET in a .NET Worker Service.

Prerequisites
    Before starting, make sure you have:
  • .NET 8 Worker Service project
  • SQL Server database
  • Basic knowledge of ADO.NET
  • Polly package installed

Install Polly package using NuGet

Before implementing resilience strategies, you need to install the Polly NuGet package in your .NET Worker Service project. Polly is a popular resilience and transient-fault-handling library for .NET that provides built-in support for Retry, Timeout, Circuit Breaker, Fallback, and other fault-handling policies. Installing this package allows your application to gracefully handle temporary database failures and improve the reliability of background services.

You can install Polly using the following .NET CLI command:

dotnet add package Polly  

Alternatively, if you are using Visual Studio, you can install it through the NuGet Package Manager by searching for the “Polly” package and adding it to your project.

Create a Worker Service Project

To begin implementing Polly in a background application, you first need to create a .NET Worker Service project. You can create a new Worker Service project using the .NET CLI with the command dotnet new worker -n PollyWorkerService, which generates the required project structure and boilerplate code.

After creation, navigate into the project folder using cd PollyWorkerService and open it in your preferred development environment. This setup provides the foundation where you can later implement database operations along with Polly’s resilience policies.

If you are using Microsoft Visual Studio then visit my previous post to create a worker service

Configure Database Connection String

  
  Add the database connection string inside appsettings.json.
  { 
  "ConnectionStrings": { 
	"DefaultConnection": "Server=YOUR_SERVER;Database=YOUR_DB;Trusted_Connection=True;TrustServerCertificate=True;" 
	} 
}

Create Polly Resilience Policies

Open Worker.cs and create Retry, Timeout, and Circuit Breaker policies.

using Microsoft.Data.SqlClient;
using Polly;
using Polly.Timeout;
using System.Data;
public class Worker : BackgroundService
{
  private readonly IConfiguration _configuration;
  private readonly ILogger<Worker> _logger;
  public Worker(IConfiguration configuration, ILogger<Worker> logger)
  {
    _configuration = configuration; _logger = logger;
  }
  protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  {
    var retryPolicy = Policy .Handle<SqlException>()
    .Or<TimeoutRejectedException>()
    .WaitAndRetryAsync(
    retryCount: 3,
    sleepDurationProvider: retryAttempt =>
    TimeSpan.FromSeconds(retryAttempt),
    onRetry: (exception, timeSpan, retryCount, context) =>
    {
      _logger.LogWarning(
      $"Retry {retryCount} after {timeSpan.TotalSeconds}s due to: {exception.Message}");
      });
      var timeoutPolicy = Policy .TimeoutAsync(TimeSpan.FromSeconds(5));
      var circuitBreakerPolicy = Policy
      .Handle<SqlException>()
      .CircuitBreakerAsync(
      exceptionsAllowedBeforeBreaking: 2,
      durationOfBreak: TimeSpan.FromSeconds(30),
      onBreak: (exception, breakDelay) =>
      {
        _logger.LogError( $"Circuit broken for {breakDelay.TotalSeconds}s due to: {exception.Message}");
        },
        onReset: () =>
        {
          _logger.LogInformation("Circuit reset.");
          });
          var combinedPolicy = Policy.WrapAsync(
          retryPolicy, circuitBreakerPolicy, timeoutPolicy);
          while (!stoppingToken.IsCancellationRequested)
          {
            try
            {
              await combinedPolicy.ExecuteAsync(async () => {
                await GetDataFromDatabaseAsync();
                });
              }
              catch (Exception ex)
              {
                _logger.LogError(ex, "Database operation failed.");
              }
              await Task.Delay(5000, stoppingToken);
            }
          }
          private async Task GetDataFromDatabaseAsync()
          {
            var connectionString = _configuration.GetConnectionString("DefaultConnection");
            using SqlConnection connection = new SqlConnection(connectionString);
            await connection.OpenAsync();
            var query = "SELECT TOP 1 * FROM Employees";
            using SqlCommand command = new SqlCommand(query, connection);
            using SqlDataReader reader = await command.ExecuteReaderAsync();
            while (await reader.ReadAsync())
            {
              Console.WriteLine(reader[0].ToString());
            }
          }
        }

Implementing Polly Strategies in .NET Worker Service with Dapper

To implement Polly resilience strategies in a .NET Worker Service that uses Dapper, the main idea is to wrap your database operations inside Polly policies (retry, circuit breaker, timeout, etc.) rather than letting Dapper handle failures directly.

Dapper is intentionally lightweight and does not provide built-in resilience mechanisms, making Polly a good companion for transient SQL failures.

Install Required Packages

Before implementing Polly with Dapper in a .NET Worker Service, you need to install a few NuGet packages that provide database access and resilience capabilities.

Dapper is a lightweight Object-Relational Mapper (ORM) that simplifies executing SQL queries and mapping results to C# objects while maintaining high performance.

Microsoft.Data.SqlClient is the official SQL Server data provider used to establish database connections and execute commands against SQL Server.

Polly is a resilience and transient-fault-handling library that helps applications recover from temporary failures by providing strategies such as retries, circuit breakers, timeouts, and fallback mechanisms.

If you're using Polly's integration features with dependency injection and the .NET hosting model, Polly.Extensions provides additional support for registering and managing resilience pipelines within the application's service container.

Together, these packages enable a Worker Service to perform database operations through Dapper while handling transient database or network issues in a robust and reliable manner.

Install them using the following commands:

dotnet add package Dapper
dotnet add package Polly
dotnet add package Polly.Extensions
dotnet add package Microsoft.Data.SqlClient

After installation, these libraries can be registered and configured in the application's dependency injection container, allowing database operations to benefit from Polly's resilience strategies such as retry and circuit breaker policies.

Configure Polly in Program.cs


using Polly;
using Polly.Retry;
using Polly.CircuitBreaker;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton(sp =>
{
  return new ResiliencePipelineBuilder()
  .AddRetry(new RetryStrategyOptions
  {
    MaxRetryAttempts = 3,
    Delay = TimeSpan.FromSeconds(2),
    BackoffType = DelayBackoffType.Exponential
    })
    .AddCircuitBreaker(new CircuitBreakerStrategyOptions
    {
      FailureRatio = 0.5,
      MinimumThroughput = 5,
      BreakDuration = TimeSpan.FromSeconds(30)
      })
      .Build();
      });
      builder.Services.AddSingleton<IDbConnectionFactory, SqlConnectionFactory>();
      builder.Services.AddScoped<IProductRepository, ProductRepository>();
      builder.Services.AddHostedService<Worker>();
      var host = builder.Build();
      host.Run();

The Program.cs file is the entry point of a .NET Worker Service and is responsible for configuring dependency injection, application services, and resilience strategies. In this step, a Polly Resilience Pipeline is registered as a singleton service in the dependency injection container so that it can be reused throughout the application.

The pipeline combines multiple resilience strategies into a single execution flow. In the example, a Retry Strategy is configured to automatically retry failed operations up to three times with an exponential backoff delay, which helps recover from temporary database connectivity issues or transient SQL Server errors.

A Circuit Breaker Strategy is also added to monitor failures and temporarily stop database requests when a predefined failure threshold is reached, preventing the application from repeatedly calling an unhealthy database and allowing it time to recover.

By registering this pipeline as a singleton, all repositories and services can use the same resilience configuration, ensuring consistent fault-handling behavior across the Worker Service. This centralized approach improves reliability, reduces duplicated error-handling code, and makes resilience settings easier to maintain and update.

Create a Connection Factory


using Microsoft.Data.SqlClient;
using System.Data;
public interface IDbConnectionFactory
{
  IDbConnection CreateConnection();
}
public class SqlConnectionFactory : IDbConnectionFactory
{
  private readonly IConfiguration _configuration;
  public SqlConnectionFactory(IConfiguration configuration)
  {
    _configuration = configuration;
  }
  public IDbConnection CreateConnection()
  {
    return new SqlConnection(
    _configuration.GetConnectionString("DefaultConnection"));
  }
}

Opening a fresh connection per operation allows connection pooling to work correctly and reduces the risk of pool exhaustion.

Wrap Dapper Calls with Polly


using Dapper;
using Polly;
public class ProductRepository : IProductRepository
{
  private readonly IDbConnectionFactory _connectionFactory;
  private readonly ResiliencePipeline _pipeline;
  public ProductRepository(
  IDbConnectionFactory connectionFactory,
  ResiliencePipeline pipeline)
  {
    _connectionFactory = connectionFactory;
    _pipeline = pipeline;
  }
  public async Task<IEnumerable<Product>> GetProductsAsync(
  CancellationToken cancellationToken)
  {
    return await _pipeline.ExecuteAsync(async token =>
    {
      using var connection =
      _connectionFactory.CreateConnection();
      const string sql =
      "SELECT Id, Name, Price FROM Products";
      return await connection.QueryAsync<Product>(sql);
      }, cancellationToken);
    }
  }

Add Logging During Retries


builder.Services.AddSingleton(sp =>
{
  var logger =
  sp.GetRequiredService<ILogger<Program>>();
  return new ResiliencePipelineBuilder()
  .AddRetry(new RetryStrategyOptions
  {
    MaxRetryAttempts = 3,
    OnRetry = args =>
    {
      logger.LogWarning(
      "Retry attempt {Retry} after {Delay}",
      args.AttemptNumber,
      args.RetryDelay);
      return ValueTask.CompletedTask;
    }
    })
    .Build();
    });

Use in a Worker Service


public class Worker : BackgroundService
{
  private readonly IProductRepository _repository;
  private readonly ILogger<Worker> _logger;
  public Worker(
  IProductRepository repository,
  ILogger<Worker> logger)
  {
    _repository = repository;
    _logger = logger;
  }
  protected override async Task ExecuteAsync(
  CancellationToken stoppingToken)
  {
    while (!stoppingToken.IsCancellationRequested)
    {
      try
      {
        var products =
        await _repository.GetProductsAsync(stoppingToken);
        _logger.LogInformation(
        "Retrieved {Count} products",
        products.Count());
      }
      catch (Exception ex)
      {
        _logger.LogError(ex,
        "Database operation failed");
      }
      await Task.Delay(
      TimeSpan.FromMinutes(1),
      stoppingToken);
    }
  }
}

Recommended Strategy for Dapper + Worker Services

Polly supports combining multiple strategies into a single resilience pipeline, including retry, timeout, circuit breaker, and rate limiting.

For production systems, a common combination is:
  1. Retry with exponential backoff for transient SQL/network failures.
  2. Timeout to prevent long-running database calls.
  3. Circuit Breaker to stop hammering an unhealthy database.
  4. Structured logging on retries and circuit transitions.
  5. Short-lived connections created per operation.

This pattern aligns with Polly's recommended resilience approach for transient-fault handling in modern .NET applications.

Implementing Polly Strategies in .NET Worker Service with Entity Framework

If you are using Entity Framework Core, you usually do not need Polly just for database retries. EF Core already provides built-in resilient execution strategies for supported providers like SQL Server.

For SQL Server, this is typically enough:

builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
connectionString,
sqlOptions =>
{
  sqlOptions.EnableRetryOnFailure(
  maxRetryCount: 5,
  maxRetryDelay: TimeSpan.FromSeconds(30),
  errorNumbersToAdd: null);
  }));
This handles common transient failures like:
  • temporary network issues
  • Azure SQL failovers
  • throttling
  • brief connectivity interruptions

Best Practices
  • Use Retry only for transient failures
  • Keep timeout duration realistic
  • Avoid excessive retries
  • Monitor circuit breaker events with logging
  • Combine Polly with health checks and observability tools

Summary

Using Polly with ADO.NET or Dapper in a .NET Worker Service significantly improves application reliability. By implementing Retry, Timeout, and Circuit Breaker strategies, you can protect your background services from transient database failures and improve system stability in production environments.

Polly provides a clean and flexible approach to resilience engineering, making it an excellent choice for enterprise-grade .NET applications.

Thanks

Kailash Chandra Behera

I am an IT professional with over 13 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال