Skip to content

Azure Cache for Redis

Key Points

  • Managed Redis. Tiers: Basic (no SLA), Standard (HA pair), Premium (clustering, persistence, vnet), Enterprise (Redis Enterprise; modules), Enterprise Flash (NVMe; cheap big cache).
  • StackExchange.Redis is the .NET client. Singleton ConnectionMultiplexer; thread-safe.
  • Idiomatic uses: distributed cache (IDistributedCache), session, SignalR backplane, pub/sub, distributed locks (carefully), rate limiting, leaderboards.
  • Avoid Redis as primary store unless using Enterprise with persistence.
  • Connection mgmt is the #1 pitfall — connection multiplexer is meant to be reused; many apps create per-request connections and exhaust.

Concepts (deep dive)

What Redis is

Redis is an in-memory data structure server. The entire dataset lives in RAM (with optional disk persistence for durability), and the server speaks a small text protocol over TCP. Unlike a relational database, Redis isn't a place you query — it's a place you put already-shaped values: strings, hashes, lists, sets, sorted sets, streams, bitmaps. Reads are typically sub-millisecond because there's no disk seek, no query planner, no join.

  application                              Redis server (single process)
  ┌──────────┐    SET k v   ┌────────────────────────────────────────┐
  │  client  │ ────────────►│   single-threaded event loop           │
  │ (multi-  │              │   ┌────────┐ ┌────────┐ ┌────────┐    │
  │ threaded)│ ◄────────────│   │ string │ │  hash  │ │  zset  │ …  │
  │          │    OK / val  │   └────────┘ └────────┘ └────────┘    │
  └──────────┘              │            RAM-resident data           │
                            │   ────────────────────────────────     │
                            │   optional: RDB snapshots + AOF log    │
                            └────────────────────────────────────────┘

Why Redis exists vs a "real" database:

  • Latency — RAM access is ~100ns; SSD ~100μs; a SQL query against a B-tree is milliseconds. For hot reads (sessions, tokens, lookup caches), the ratio is 1000–10,000× faster.
  • Right-shaped data — counters, leaderboards, queues, locks, pub/sub channels, geo indexes are primitives in Redis, not awkward emulations on top of tables.
  • Throughput — a single Redis node sustains 100k+ ops/sec because the event loop never blocks on I/O.

The trade-off: RAM is expensive and finite, and Basic/Standard tiers can lose data on restart. Redis is a cache, a coordination layer, or a real-time index — not a system of record for anything you can't reconstruct.

Single-threaded event loop (the consequence)

Each Redis server process handles one command at a time on a single CPU thread. This is the source of two seemingly contradictory properties: every command is atomic with respect to every other command (no race conditions between operations), and one slow command (KEYS *, a giant SORT, a malformed Lua script) blocks every other client until it finishes.

This is also why ConnectionMultiplexer is built the way it is: the client side is multi-threaded, so it pipelines many commands over a few long-lived TCP connections rather than opening a connection per call. One multiplexer can serve a whole app pool.

💡 The corollary for production: avoid O(N) commands on the whole keyspace (KEYS, FLUSHDB, large HGETALLs). Use SCAN for iteration; cap value sizes.

Tiers

The Azure tier you pick controls what kind of Redis you get — single node vs replicated, vanilla open-source Redis vs Redis Enterprise (with modules), and whether features like clustering, persistence, VNet integration, and geo-replication are on the table. Pick by what failure modes you can tolerate, not by price alone.

Tier Notes
Basic Single node; no SLA; dev only
Standard Primary+replica; 99.9% SLA
Premium Clustering, persistence, vnet, geo-replication
Enterprise Redis Enterprise; modules (Search, JSON, TimeSeries)
Enterprise Flash Big caches at lower cost (NVMe)

Connection

ConnectionMultiplexer is the client's permanent, thread-safe pipe to the Redis server: it owns the TCP connection(s), the reconnect logic, the command pipeline, and the response demultiplexer. Creating one is expensive (DNS, TLS handshake, AUTH, topology discovery); using one is cheap. The whole library is designed around the assumption that you create exactly one and share it.

builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
    ConnectionMultiplexer.Connect("myredis.redis.cache.windows.net:6380,password=...,ssl=True"));

public class C(IConnectionMultiplexer mux)
{
    private readonly IDatabase _db = mux.GetDatabase();
    public Task SetAsync(string k, string v) => _db.StringSetAsync(k, v);
}

Critical: ConnectionMultiplexer is heavy; create once. GetDatabase() is cheap.

Microsoft.Extensions.Caching.StackExchangeRedis

IDistributedCache is ASP.NET Core's abstraction for "a cache that survives a single process" — same shape as IMemoryCache, but every operation is async and crosses the network. The StackExchangeRedis package implements that interface against a Redis backend, so any framework code that already takes IDistributedCache (output caching, response caching, session state) immediately works against Redis.

builder.Services.AddStackExchangeRedisCache(o =>
{
    o.Configuration = "myredis...";
    o.InstanceName = "myapp:";
});

// Use IDistributedCache
public class C(IDistributedCache cache) { /* ... */ }

For HybridCache integration, see Caching Strategies.

Common patterns

Cache-aside

The application owns the cache: read from cache first, on miss read from source and populate cache. Redis is dumb storage; the app decides when to refresh, when to expire, what to serialize. This is the most common Redis pattern by far and the one HybridCache automates.

var json = await db.StringGetAsync(key);
if (!json.HasValue)
{
    var fresh = await Load();
    await db.StringSetAsync(key, JsonSerializer.Serialize(fresh), TimeSpan.FromHours(1));
    return fresh;
}
return JsonSerializer.Deserialize<T>(json!);

Pub/Sub

A fire-and-forget broadcast channel: publishers send messages to a named channel, all currently-subscribed subscribers receive them, and there is no buffering — messages sent while a subscriber is offline are lost forever. Useful for cache invalidation fanout, SignalR backplane, and other "tell everyone right now" flows; useless when you need durability.

var sub = mux.GetSubscriber();
await sub.SubscribeAsync(RedisChannel.Literal("orders"), (ch, msg) => Process(msg));
await sub.PublishAsync(RedisChannel.Literal("orders"), "payload");

Lightweight; no persistence. For durable, use Service Bus.

Distributed lock

The pattern: SET key value NX EX 30 — atomic "set only if not exists, with TTL." Whoever wins the SET holds the lock; everyone else retries or gives up. The TTL is essential — if the holder crashes without releasing, the lock unblocks automatically. The Lua script on release is also essential: it deletes the lock only if you still own it, preventing one client from accidentally releasing another's lock after their TTL expired.

⚠️ Even this pattern has known correctness gaps under network partitions and process pauses. For anything truly safety-critical, use a vetted library (RedLock.net) or push the consensus problem to a real coordination service.

// Set with NX (only if not exists) + EX (TTL)
var acquired = await db.StringSetAsync(lockKey, requestId, TimeSpan.FromSeconds(30), When.NotExists);
if (!acquired) return /* already locked */;
try { /* critical section */ }
finally
{
    // Lua-script-released to ensure we own it
    await db.ScriptEvaluateAsync("if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end",
        new RedisKey[] { lockKey }, new RedisValue[] { requestId });
}

Use RedLock for distributed correctness across multiple Redis nodes. Locks have edge cases — use Service Bus sessions or DB locks for critical correctness.

Rate limiter

Atomic INCR on a per-key counter, set a TTL the first time it's bumped, reject when the counter exceeds the budget. Because Redis is single-threaded, the INCR+EXPIRE pair is race-free even under concurrent requests from many app instances.

// Token bucket via INCR + EXPIRE
var current = await db.StringIncrementAsync(rateKey);
if (current == 1) await db.KeyExpireAsync(rateKey, TimeSpan.FromMinutes(1));
if (current > 100) return TooManyRequests();

ASP.NET Core's built-in rate limiter is per-instance; Redis-backed shares across.

Persistence (Premium+)

By default Redis is pure RAM — a restart wipes everything. Persistence makes a copy on disk so the cache survives planned and unplanned restarts. Two mechanisms, often combined:

  • RDB: snapshots periodically.
  • AOF: append every write to log.

For Premium, enable persistence to survive restarts. Without, data is volatile.

Clustering (Premium+)

Beyond a single node's RAM, Redis shards the keyspace across multiple primary nodes using a fixed 16,384-slot hash ring; each key hashes to a slot and lives on whichever node owns that slot. The client (via ConnectionMultiplexer) learns the topology and routes commands to the right node automatically. Multi-key operations (MGET, transactions, scripts touching multiple keys) only work when all involved keys live on the same node — the {...} hashtag forces them to.

ConnectionMultiplexer.Connect("host1:6380,host2:6380,host3:6380,...");

Sharded; single key = one shard. Multi-key ops require keys on same shard via hashtag: {user:123}:profile, {user:123}:settings.

Geo-replication

Premium tier: passive geo-replica in another region. Failover via API.

Encryption

In-transit: TLS (port 6380). At-rest: encryption enabled by default in Azure.

Authentication

Two ways to prove you're allowed to talk to Redis. Access keys are the legacy default — long random strings, rotated manually, often stashed in app config. Entra ID uses the same managed-identity flow as the rest of Azure: no shared secret to leak, no rotation to schedule, RBAC controls who can connect. For new work, prefer Entra ID.

  • Access keys (default): primary + secondary; rotate.
  • Microsoft Entra ID (preview/GA): managed identity authentication.
// Entra ID auth
var options = ConfigurationOptions.Parse("redis...");
options.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());
var mux = await ConnectionMultiplexer.ConnectAsync(options);

TTL strategies

Every cache entry needs an expiration policy because RAM is finite. Absolute TTL expires N seconds after the value was written — predictable, simple. Sliding TTL resets the expiration on every access — keeps hot items warm, lets cold items decay. Pick absolute for "data is stale after N minutes regardless"; pick sliding for "evict if nobody's asked recently."

await db.StringSetAsync(key, value, TimeSpan.FromHours(1));    // absolute
await db.KeyExpireAsync(key, TimeSpan.FromMinutes(5));         // sliding (call on access)

Memory pressure

When Redis hits maxmemory, it has to choose: refuse new writes, or evict existing keys. The eviction policy picks which. For caches, you almost always want allkeys-lru (kick the least-recently-used key on every write); noeviction (errors when full) is what you get out of the box and is rarely what you want.

INFO memory
used_memory: 8GB / 13GB max

When full: eviction policy (maxmemory-policy): - noeviction (errors). - allkeys-lru (best for cache). - volatile-lru (only TTL keys). - allkeys-random.

For caches, allkeys-lru is the default.

Common pitfalls

  • Per-call ConnectionMultiplexer — expensive; exhausts.
  • No timeout — hung calls block.
  • Non-async — blocks threads.
  • Treating Redis as primary store — lose data on restart (Basic/Standard).
  • Distributed lock incorrectness — many subtle bugs; use a library.

.NET 8+ HybridCache

HybridCache is the modern .NET caching abstraction: a two-tier cache that keeps a fast in-process L1 (memory) in front of a slower distributed L2 (Redis), with stampede protection (only one caller fills the cache on a miss; the others await) and tag-based invalidation built in. It replaces the hand-rolled cache-aside loops everyone was writing.

builder.Services.AddHybridCache()
    .AddRedis(connStr);

Two-tier (local memory + Redis) with stampede protection. Recommended over manual cache-aside.


Code: correct vs wrong

❌ Wrong: per-call connection

public Task M() { var mux = ConnectionMultiplexer.Connect(...); ... }

✅ Correct: singleton

builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(...));

❌ Wrong: no TTL

await db.StringSetAsync(k, v);   // forever

✅ Correct: TTL

await db.StringSetAsync(k, v, TimeSpan.FromHours(1));

Design patterns for this topic

Pattern 1 — "Singleton ConnectionMultiplexer"

  • Intent: reuse; thread-safe.

Pattern 2 — "HybridCache with Redis L2"

  • Intent: two-level cache.

Pattern 3 — "Pub/Sub for SignalR backplane"

  • Intent: scale-out real-time.

Pattern 4 — "RedLock library for distributed lock"

  • Intent: correct distributed locking.

Pattern 5 — "Rate limiter via INCR+EXPIRE"

  • Intent: distributed rate limiting.

Pros & cons / trade-offs

Tier Pros Cons
Basic Cheap No SLA
Standard HA No clustering
Premium Cluster + persistence Cost
Enterprise Modules $$$

When to use / when to avoid

  • Use for cache, sessions, SignalR backplane, pub/sub.
  • Use Premium for production with persistence.
  • Avoid as primary store on Basic/Standard.
  • Avoid distributed locks for safety-critical without RedLock.

Interview Q&A

Q1. Tier choice? Standard for prod default; Premium for cluster/persistence/vnet; Enterprise for modules.

Q2. ConnectionMultiplexer lifetime? Singleton. Heavy connection; thread-safe; reuse.

Q3. Idiomatic use cases? Cache, sessions, pub/sub, rate limiter, SignalR backplane.

Q4. Persistence? Premium tier. RDB snapshots + AOF append log.

Q5. Clustering? Sharded. Multi-key ops need same shard via hashtag {...}.

Q6. Distributed lock? Set NX EX pattern. Use RedLock library for correctness across nodes.

Q7. Pub/Sub vs Service Bus? Pub/Sub: lightweight; no persistence. Service Bus: durable; rich features.

Q8. Eviction policy? allkeys-lru default for caches. Noeviction errors when full.

Q9. Geo-replication? Premium passive replica in another region. API failover.

Q10. Entra ID auth? Managed identity instead of access keys. Preferred.

Q11. Memory limits? Tier-dependent. Monitor; expand or evict.

Q12. HybridCache? .NET 9 two-tier (memory + Redis); stampede protection; preferred.


Gotchas / common mistakes

  • ⚠️ Per-call multiplexer — exhaustion.
  • ⚠️ Sync calls — thread blocking.
  • ⚠️ No TTL — memory grows.
  • ⚠️ Treating Redis as DB on Basic/Standard.
  • ⚠️ Distributed lock without library — bugs.

Further reading