All Blogs Technology 5 min read

Redis Caching Patterns for High-Traffic ASP.NET Core Applications

Sandeep Pal
June 3, 2026
Redis Caching Patterns for High-Traffic ASP.NET Core Applications

When your database becomes the bottleneck

High-traffic ASP.NET Core apps—e-commerce flash sales, live course launches, leaderboard refreshes—often die on repeated identical reads long before CPU maxes out. SQL Server is excellent at durable writes; it is expensive for hot read paths that tolerate seconds of staleness. Redis gives you sub-millisecond keyed lookups, atomic counters, and pub/sub for invalidation signals. The hard part is choosing patterns that stay correct under failure, not wiring AddStackExchangeRedisCache.

This guide covers patterns we use on production APIs serving millions of cache hits per day, including stampede protection and how AI recommendation endpoints should not bypass cache discipline.

Foundation: IDistributedCache vs direct Redis

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:Connection"];
    options.InstanceName = "lms:";
});

public class CourseCatalogService
{
    private readonly IDistributedCache _cache;
    private readonly AppDbContext _db;

    public async Task<CourseDto?> GetAsync(Guid id, CancellationToken ct)
    {
        var key = $"course:{id}";
        var cached = await _cache.GetStringAsync(key, ct);
        if (cached != null)
            return JsonSerializer.Deserialize<CourseDto>(cached);

        var course = await _db.Courses.AsNoTracking()
            .Where(c => c.Id == id)
            .Select(c => new CourseDto(c.Id, c.Title, c.Slug))
            .FirstOrDefaultAsync(ct);

        if (course != null)
        {
            await _cache.SetStringAsync(key, JsonSerializer.Serialize(course),
                new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }, ct);
        }
        return course;
    }
}

InstanceName prefixes keys—critical when multiple apps share a cluster. For advanced structures (sorted sets, HyperLogLog), use IConnectionMultiplexer directly.

Cache-aside (lazy loading)

Application checks cache, on miss loads DB and populates cache. Simplest pattern; you own invalidation. On write, delete keys or publish invalidation events. Risk: thundering herd when a popular key expires.

Read-through

Cache library loads on miss transparently. In .NET you often implement read-through in a thin repository wrapper rather than a proprietary server feature. Good for stable read models like "course metadata by slug."

Write-through and write-behind

Write-through updates cache and DB synchronously—consistent but slower writes. Write-behind queues DB updates—higher throughput, harder recovery. Use write-through for inventory counts you cannot oversell; avoid write-behind until you have idempotent consumers and replay tooling.

Stampede protection (must-have for hot keys)

public async Task<string?> GetWithLockAsync(string key, Func<Task<string?>> factory, CancellationToken ct)
{
    var value = await _cache.GetStringAsync(key, ct);
    if (value != null) return value;

    await using var redLock = await _lockFactory.CreateLockAsync($"lock:{key}", TimeSpan.FromSeconds(30));
    if (!redLock.IsAcquired) { await Task.Delay(50, ct); return await _cache.GetStringAsync(key, ct); }

    value = await _cache.GetStringAsync(key, ct);
    if (value != null) return value;

    value = await factory();
    if (value != null)
        await _cache.SetStringAsync(key, value, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
        }, ct);
    return value;
}

Alternatively use per-key mutex in memory for single-instance dev only—production needs distributed locks (RedLock.net) or single-flight via FusionCache library.

TTL and freshness strategy

  • Absolute expiration — simple; good for catalog pages with known refresh tolerance.
  • Sliding expiration — extends on access; watch memory growth on unbounded key spaces.
  • Jitter — add random seconds to TTL so thousands of keys do not expire simultaneously.
  • Version stamps — embed v:{publishVersion} in keys; bump version on bulk content deploy instead of scanning deletes.

Invalidation patterns

Delete-on-write for entity keys: course:{id}, course:slug:{slug}. For related lists, maintain a set of list keys in Redis updated on write, or use short TTL on list endpoints and accept eventual consistency. Pub/Sub: publisher sends invalidate:course:guid; all API instances evict local memory + distributed keys if using hybrid L1/L2.

What not to cache

Personalized gradebooks with strict authorization, one-time payment intents, and CSRF tokens. Never cache responses that vary only by JWT without including tenant and user in the key—cache poisoning across users is a career-limiting event.

Session and rate limiting

Redis excels at sliding window rate limits and refresh token blocklists. Keep sessions small; store user id and claims version, load details from DB on demand.

Observability

Track hit ratio, latency p99, evicted keys, and connected clients. Alert when hit ratio drops after a deploy—often a serialization change or key prefix bug. Use SLOWLOG and memory limits (maxmemory-policy allkeys-lru) with intentional caps.

High availability

Azure Cache for Redis Enterprise or clustered OSS with replication. Client-side retry with exponential backoff on connection blips. Circuit-breaker to database when Redis is down—degraded mode beats total outage if you throttle.

AI workloads and cache

Embedding vectors and retrieval results can be cached by hash of normalized query text—massive cost savings for tutoring bots. Do not cache raw user PII in keys. Separate Redis database index for AI vs session data. Invalidate when underlying course content changes via the same version stamp mechanism.

Load test script mindset

Before Black Friday or course launch, replay 10x read traffic against staging with Redis enabled and disabled. Compare SQL batch requests and p95 API latency. Tune connection multiplexer timeouts (connectTimeout, syncTimeout)—defaults hurt under burst.

Redis caching for ASP.NET Core is not "turn it on and forget." Cache-aside with jittered TTL, explicit invalidation, and stampede protection handles most high-traffic shapes. Ship the patterns in one service first, measure hit ratio for a week, then standardize via a shared caching package across your APIs.

1 views 0 likes 0 comments
Comments (0)
Sign in to leave a comment
Toolliyo Assistant
Ask about tutorials, ebooks, training, pricing, mentor services, and support. I use public site content only—not admin or internal tools.

care@toolliyo.com

Need callback? Share your details