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.
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.
Install the Redis distributed cache package and the underlying Redis client:
bashdotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package StackExchange.Redis
Register the cache in 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:
bashdocker run -d --name redis -p 6379:6379 redis:latest
Once registered, inject IDistributedCache anywhere via DI. It stores
byte[] values, so most apps wrap it with JSON serialization helpers:
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.
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:
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:";
});
| Option | Purpose |
|---|---|
AbortOnConnectFail | Lets the app start even if Redis is briefly unavailable, instead of throwing immediately. |
ConnectRetry | Number of times to retry the initial connection. |
ReconnectRetryPolicy | Backoff strategy used while attempting to restore a dropped connection. |
SyncTimeout | Maximum 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.
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.
dotnet add package Polly.Core
dotnet add package Polly.Extensions
Register a named pipeline combining retry, circuit breaker, and timeout strategies:
C# — Program.csusing 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:
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;
}
}
Register the service and pipeline together so controllers stay simple:
C# — Program.csbuilder.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);
}
For whole-response caching rather than data caching, ASP.NET Core's Output Caching middleware can also be backed by Redis:
bashdotnet 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.
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.
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics => metrics.AddMeter("Polly"))
.WithTracing(tracing => tracing.AddSource("Polly"));
InstanceName or a key prefix convention)
so multiple services sharing one Redis instance don't collide.
Also Read: Build an OpenID Connect Application in ASP.NET Core
AI-generated fashion design is revolutionizing the industry, pushing the boundaries of creativity and innovation in style.
Browse most trending Journalist jobs in South Africa. Apply today
Browse most trending Photographer jobs in South Africa. Apply today
Browse most trending Fashion jobs in United Kingdom. Apply today
AI-Generated Fashion Design
SEP 10, 2026
Streetwear Luxury Collaborations
SEP 10, 2026 5,617 VIEWS
Get the latest creative news from FooBar about art, design and business.
Subscribe