StackExchange.Redis Best Practices in ASP.NET Core

 


Singleton Multiplexer, Timeouts, Reconnect Behaviour, and Health Checks

If Redis is “just a cache”, you can get away with sloppy usage.

If Redis is part of your system designrate limiting, coordination, read models, eventing — then client configuration matters as much as schema design.

Most Redis production incidents in .NET are not caused by Redis itself.
They’re caused by how the client is used.

This article shows how to use StackExchange.Redis correctly in ASP.NET Core:

  • one multiplexer (always)

  • explicit timeouts

  • predictable reconnect behaviour

  • health checks that actually mean something


The One Rule You Must Not Break

ConnectionMultiplexer is expensive — create one and reuse it.

❌ The classic mistake

using var redis = ConnectionMultiplexer.Connect(connectionString);

Per request. Per operation. Per repository.

This:

  • creates multiple TCP connections

  • multiplies reconnect storms

  • kills throughput under load

✅ The correct mental model

ConnectionMultiplexer is:

  • thread-safe

  • designed to be shared

  • internally multiplexes commands over connections

Create one per app.


Registering the Multiplexer Correctly

Recommended setup

builder.Services.AddSingleton<IConnectionMultiplexer>(sp => { var configuration = ConfigurationOptions.Parse( builder.Configuration.GetConnectionString("Redis")); configuration.AbortOnConnectFail = false; configuration.ConnectRetry = 3; configuration.ConnectTimeout = 5000; configuration.SyncTimeout = 5000; return ConnectionMultiplexer.Connect(configuration); });

Why these options matter

  • AbortOnConnectFail = false
    Allows the app to start even if Redis is temporarily unavailable.

  • ConnectRetry
    Retries initial connection.

  • ConnectTimeout
    Fails fast instead of hanging startup.

  • SyncTimeout
    Prevents blocking calls from stalling threads.


Always Work with IDatabase

Never store the multiplexer everywhere.

public class CacheService { private readonly IDatabase _db; public CacheService(IConnectionMultiplexer mux) { _db = mux.GetDatabase(); } }

IDatabase:


Async First (Seriously)

❌ Avoid this

var value = db.StringGet("key");

Under load:

  • blocks threads

  • increases latency

  • amplifies timeout issues

✅ Prefer async

var value = await db.StringGetAsync("key");

Redis is fast - your app threads are not infinite.


Timeouts: Your First Safety Net

Redis is fast but networks are not

You need both:

  • connect timeout

  • operation timeout

Recommended baseline

configuration.ConnectTimeout = 5000; configuration.SyncTimeout = 5000;

For async-heavy apps:

  • SyncTimeout mostly protects accidental sync calls

  • Async ops respect cancellation tokens

Fail fast > hang forever.


Reconnect Behaviour: Understand the Contract

Redis clients do reconnect automatically, but:

  • in-flight commands may fail

  • timeouts will still surface

  • reconnects are not “free”

Key settings

configuration.AbortOnConnectFail = false; configuration.KeepAlive = 10; configuration.ReconnectRetryPolicy = new ExponentialRetry(5000);

This:

  • keeps connections warm

  • avoids thundering reconnect storms

  • smooths transient failures

Important rule

Your app must:

  • tolerate Redis being temporarily unavailable

  • degrade gracefully

  • never assume Redis is always there


Handling Failures Gracefully

Redis failures should not crash your app.

Example: fail-open cache

public async Task<T?> GetAsync<T>(string key) { try { var value = await _db.StringGetAsync(key); return value.HasValue ? JsonSerializer.Deserialize<T>(value!) : default; } catch (RedisException) { return default; } }

Redis is an optimisation layer - not your source of truth.


Connection Events: Logging What Matters

StackExchange.Redis exposes useful events.

mux.ConnectionFailed += (_, e) => { logger.LogWarning( "Redis connection failed: {Endpoint} {FailureType}", e.EndPoint, e.FailureType); }; mux.ConnectionRestored += (_, e) => { logger.LogInformation( "Redis connection restored: {Endpoint}", e.EndPoint); };

Log:

  • failures

  • restorations

  • not every reconnect attempt

Signal > noise.


Health Checks: Don’t Lie to Yourself

❌ Bad health check

“Redis connected at startup.”

This tells you nothing.

✅ Meaningful health check

builder.Services.AddHealthChecks() .AddRedis( builder.Configuration.GetConnectionString("Redis"), name: "redis", timeout: TimeSpan.FromSeconds(3));

Better: custom ping check

public class RedisHealthCheck : IHealthCheck { private readonly IConnectionMultiplexer _mux; public RedisHealthCheck(IConnectionMultiplexer mux) { _mux = mux; } public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken) { try { var db = _mux.GetDatabase(); await db.PingAsync(); return HealthCheckResult.Healthy(); } catch (Exception ex) { return HealthCheckResult.Unhealthy( "Redis unavailable", ex); } } }

This tests:

  • connectivity

  • command execution

  • real behaviour


Threading Model: Why Redis Feels “Weird” Under Load

Redis is:

  • single-threaded per instance

  • extremely fast per operation

Your client:

Problems arise when:

  • commands take too long

  • backlogs form

  • timeouts cascade

Design takeaway

  • keep commands small

  • avoid large payloads

  • don’t treat Redis as a blob store


Common Anti-Patterns (Avoid These)

❌ Multiple multiplexers per app
❌ Blocking sync calls
❌ No timeouts
❌ Treating Redis outages as fatal
❌ Large values and massive keys
❌ Assuming Redis == durability

All of these work… until production.


Recommended Production Defaults

For most ASP.NET Core services:

  • Singleton ConnectionMultiplexer

  • Async everywhere

  • ConnectTimeout: 5s

  • SyncTimeout: 5s

  • AbortOnConnectFail: false

  • Health checks that ping

  • Graceful degradation


Why This Article Matters

Redis problems don’t usually show up in development.

They show up when:

  • traffic spikes

  • Redis restarts

  • networks wobble

  • caches miss simultaneously

Correct client usage turns Redis from:

“that thing that occasionally breaks prod”

into:

“a boring, reliable building block”

And boring is good.

Comments

Popular posts from this blog

Using a Semantic Model as a Reasoning Layer (Not Just Metadata)

A Thought Experiment: What If Analytics Models Were Semantic, Not Structural?

Dev Tunnels with Visual Studio 2022 and Visual Studio 2026