• Home
  • /
  • Articles
  • /
  • Software Development
  • /
  • Redis Caching in ASP.NET Core
HEADLINES

Redis Caching in ASP.NET Core with Resilience Policies

How to add Redis-backed distributed caching to an ASP.NET Core app, and wrap it in Polly resilience policies — retry, circuit breaker, timeout, and fallback — so a slow or unavailable cache never takes your app down with it.


  • Techm Studios
  • BY TECHM STUDIOS
  • |
  • Sept 09, 2026
  • |
  • UPDATED: Sept 09, 2026
  • |
  • 0 COMMENTS
Redis Caching in ASP.NET Core
© TheCodeBuzz / Images

ASP.NET Core · Caching · Resilience
.NET 8 / .NET 9+ StackExchange.Redis Polly v8

01Why Cache With Redis — and Why Add Resilience?

Redis is a fast, in-memory data store commonly used as a distributed cache in ASP.NET Core applications. Unlike an in-process memory cache, a Redis cache is shared across all instances of your app — useful once you scale to more than one server or container. ASP.NET Core supports this out of the box through the IDistributedCache abstraction, backed by the Microsoft.Extensions.Caching.StackExchangeRedis package.

The catch: Redis is a network dependency. If the Redis server is slow, restarting, or briefly unreachable, a naive cache call can throw an exception or hang — and if every request path depends on the cache, a Redis blip can cascade into an application-wide outage. That's where resilience policies come in: retries for transient blips, a circuit breaker to stop hammering a struggling Redis instance, timeouts so a slow cache never blocks a request indefinitely, and a fallback that lets the app degrade gracefully (e.g. read straight from the database) instead of failing outright.

Key idea: a cache should make your app faster, never less reliable. Resilience policies keep a cache outage from becoming an application outage.

02Setting Up Redis Caching in ASP.NET Core

Install the Redis distributed cache package and the underlying Redis client:

bash
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package StackExchange.Redis

Register the cache in Program.cs:

C# — Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "MyApp:";
});

builder.Services.AddControllers();

var app = builder.Build();
app.MapControllers();
app.Run();
appsettings.json
{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  }
}

Run Redis locally with Docker if you don't already have an instance:

bash
docker run -d --name redis -p 6379:6379 redis:latest

03Using IDistributedCache

Once registered, inject IDistributedCache anywhere via DI. It stores byte[] values, so most apps wrap it with JSON serialization helpers:

C#
public class ProductsController : ControllerBase
{
    private readonly IDistributedCache _cache;

    public ProductsController(IDistributedCache cache) => _cache = cache;

    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var cacheKey = $"product:{id}";
        var cached = await _cache.GetStringAsync(cacheKey);

        if (cached is not null)
            return Ok(JsonSerializer.Deserialize<Product>(cached));

        var product = await _repository.GetByIdAsync(id);
        if (product is null) return NotFound();

        await _cache.SetStringAsync(
            cacheKey,
            JsonSerializer.Serialize(product),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
                SlidingExpiration = TimeSpan.FromMinutes(2)
            });

        return Ok(product);
    }
}

This works, but it has no protection against Redis being briefly unreachable — GetStringAsync or SetStringAsync will throw a RedisConnectionException and, without handling, take the whole request down with it.

04Connection-Level Resilience (StackExchange.Redis)

The first layer of resilience lives in the Redis client connection itself. Configure ConfigurationOptions directly instead of a plain connection string to control reconnect behavior:

C# — Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.ConfigurationOptions = new ConfigurationOptions
    {
        EndPoints = { "localhost:6379" },
        AbortOnConnectFail = false,   // don't throw on startup if Redis isn't up yet
        ConnectRetry = 3,
        ConnectTimeout = 5000,
        SyncTimeout = 3000,
        ReconnectRetryPolicy = new ExponentialRetry(deltaBackOffMilliseconds: 1000)
    };
    options.InstanceName = "MyApp:";
});
OptionPurpose
AbortOnConnectFailLets the app start even if Redis is briefly unavailable, instead of throwing immediately.
ConnectRetryNumber of times to retry the initial connection.
ReconnectRetryPolicyBackoff strategy used while attempting to restore a dropped connection.
SyncTimeoutMaximum time to wait for a synchronous operation before timing out.

This handles the underlying connection, but it doesn't retry individual cache operations, add a circuit breaker, or provide a fallback path — that's the job of an application-level resilience pipeline.

05Adding a Polly Resilience Pipeline

Polly is the standard .NET library for resilience and transient-fault handling. Version 8 introduced a unified ResiliencePipeline API that replaces the older policy objects and is the version to use going forward.

bash
dotnet add package Polly.Core
dotnet add package Polly.Extensions

Register a named pipeline combining retry, circuit breaker, and timeout strategies:

C# — Program.cs
using Polly;
using Polly.CircuitBreaker;
using Polly.Retry;
using Polly.Timeout;
using StackExchange.Redis;

builder.Services.AddResiliencePipeline("redis-cache", pipelineBuilder =>
{
    pipelineBuilder
        .AddRetry(new RetryStrategyOptions
        {
            ShouldHandle = new PredicateBuilder()
                .Handle<RedisConnectionException>()
                .Handle<RedisTimeoutException>()
                .Handle<TimeoutRejectedException>(),
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            Delay = TimeSpan.FromMilliseconds(200),
            UseJitter = true
        })
        .AddCircuitBreaker(new CircuitBreakerStrategyOptions
        {
            ShouldHandle = new PredicateBuilder()
                .Handle<RedisConnectionException>()
                .Handle<RedisTimeoutException>(),
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(10),
            MinimumThroughput = 8,
            BreakDuration = TimeSpan.FromSeconds(30)
        })
        .AddTimeout(TimeSpan.FromSeconds(2));
});

Each strategy solves a distinct problem:

  • Retry — smooths over brief, transient failures (a dropped packet, a momentary reconnect) with exponential backoff and jitter to avoid retry storms.
  • Circuit breaker — once failures cross a threshold, it stops sending requests to Redis for a cool-down period, so a struggling cache isn't pounded by retries from every in-flight request.
  • Timeout — bounds how long any single cache call can take, so a hanging Redis connection can't stall a request indefinitely.

06Cache-Aside Pattern With Resilience + Fallback

The most important resilience decision is what happens when Redis is unavailable even after retries and the circuit is open: the app should fall back to the source of truth (typically the database) rather than fail the request. This keeps the cache as a pure performance optimization instead of a hard dependency.

C#
public class ProductCacheService
{
    private readonly IDistributedCache _cache;
    private readonly ResiliencePipeline _pipeline;
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductCacheService> _logger;

    public ProductCacheService(
        IDistributedCache cache,
        ResiliencePipelineProvider<string> pipelineProvider,
        IProductRepository repository,
        ILogger<ProductCacheService> logger)
    {
        _cache = cache;
        _pipeline = pipelineProvider.GetPipeline("redis-cache");
        _repository = repository;
        _logger = logger;
    }

    public async Task<Product?> GetProductAsync(int id, CancellationToken ct)
    {
        var cacheKey = $"product:{id}";

        try
        {
            var cached = await _pipeline.ExecuteAsync(
                async token => await _cache.GetStringAsync(cacheKey, token),
                ct);

            if (cached is not null)
                return JsonSerializer.Deserialize<Product>(cached);
        }
        catch (BrokenCircuitException)
        {
            _logger.LogWarning("Redis circuit open; reading {ProductId} from source.", id);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Cache read failed for {ProductId}; falling back to source.", id);
        }

        // Fallback: source of truth
        var product = await _repository.GetByIdAsync(id);
        if (product is null) return null;

        // Best-effort write-back — never let a cache write fail the request
        try
        {
            await _pipeline.ExecuteAsync(async token =>
                await _cache.SetStringAsync(
                    cacheKey,
                    JsonSerializer.Serialize(product),
                    new DistributedCacheEntryOptions
                    {
                        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
                    },
                    token), ct);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Cache write failed for {ProductId}.", id);
        }

        return product;
    }
}
Important: cache writes should almost always be best-effort and non-blocking for the response — a failed cache write is far less harmful than a failed cache read blocking the whole request.

07Putting It Together: A Resilient Cache Service

Register the service and pipeline together so controllers stay simple:

C# — Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.ConfigurationOptions = new ConfigurationOptions
    {
        EndPoints = { builder.Configuration["Redis:Host"]! },
        AbortOnConnectFail = false,
        ConnectRetry = 3,
        ReconnectRetryPolicy = new ExponentialRetry(1000)
    };
});

builder.Services.AddResiliencePipeline("redis-cache", pipelineBuilder =>
{
    pipelineBuilder
        .AddRetry(new RetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true
        })
        .AddCircuitBreaker(new CircuitBreakerStrategyOptions { BreakDuration = TimeSpan.FromSeconds(30) })
        .AddTimeout(TimeSpan.FromSeconds(2));
});

builder.Services.AddScoped<ProductCacheService>();

And the controller simply consumes the resilient service:

C#
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id, CancellationToken ct)
{
    var product = await _productCacheService.GetProductAsync(id, ct);
    return product is null ? NotFound() : Ok(product);
}

08Bonus: Redis-Backed Output Caching

For whole-response caching rather than data caching, ASP.NET Core's Output Caching middleware can also be backed by Redis:

bash
dotnet add package Microsoft.AspNetCore.OutputCaching.StackExchangeRedis
C# — Program.cs
builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("LongCache", policy => policy.Expire(TimeSpan.FromHours(1)));
});

builder.Services.AddStackExchangeRedisOutputCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "OutputCache:";
});

var app = builder.Build();
app.UseOutputCache();
C#
[OutputCache(PolicyName = "LongCache")]
[HttpGet("/api/products")]
public async Task<IActionResult> GetProducts()
{
    var products = await _productService.GetAllAsync();
    return Ok(products);
}

Output caching sits above the resilience pipeline configured for AddStackExchangeRedisCache, so it benefits from the same connection-level reconnect settings but doesn't go through your custom Polly pipeline directly.

09Observability

Polly's DI integration emits telemetry automatically when combined with Polly.Extensions, including resilience-event counters and activity spans you can wire into OpenTelemetry. Pair this with StackExchange.Redis's own IConnectionMultiplexer.GetCounters() to track connection health, and log every retry, circuit-break, and fallback so cache degradation is visible before it becomes a user-facing incident.

C#
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics.AddMeter("Polly"))
    .WithTracing(tracing => tracing.AddSource("Polly"));

10Best Practices

  • Never let a cache failure become a request failure — always fall back to the source of truth on a broken circuit or exhausted retries.
  • Keep timeouts short for cache calls (1–2 seconds is typical); a cache that's slower than the database defeats its own purpose.
  • Tune circuit breaker thresholds to your traffic volume — too sensitive and you'll trip on normal blips; too lax and you'll keep hammering a down instance.
  • Use jittered exponential backoff on retries to avoid synchronized retry storms across many app instances.
  • Make writes best-effort; log failures but don't block the response on a successful cache write.
  • Set sensible TTLs (absolute and/or sliding expiration) to bound both staleness and Redis memory usage.
  • Namespace your keys (InstanceName or a key prefix convention) so multiple services sharing one Redis instance don't collide.
Written for ASP.NET Core (.NET 8 / .NET 9+) with the Polly v8 resilience API. Code samples follow current Microsoft Learn and Polly documentation conventions and are intended as a practical reference, not a verbatim reproduction of any single source.

Also Read: Build an OpenID Connect Application in ASP.NET Core

SPONSORED ADS

COMMENTS
LEAVE A REPLY
SPONSORED ADS
POPULAR
AI-Generated Fashion Design

AI-generated fashion design is revolutionizing the industry, pushing the boundaries of creativity and innovation in style.


SPONSORED JOBS

TECHM JOBS
POPULAR
Journalist Jobs, South Africa

Browse most trending Journalist jobs in South Africa. Apply today

Photographer Jobs, South Africa

Browse most trending Photographer jobs in South Africa. Apply today

Graphical Designer Jobs, United Kingdom

Browse most trending Fashion jobs in United Kingdom. Apply today


TECHM PRODUCTS


TOP POSTS
STAY IN TOUCH
SPONSORED ADS
Subscribe to Updates

Get the latest creative news from FooBar about art, design and business.

Subscribe