Azure Cache for Redis: Production Caching Patterns

A caching layer is the cheapest performance upgrade most applications can buy, and Redis is the default answer. On Azure that now means choosing between the classic Azure Cache for Redis tiers and the newer Azure Managed Redis offering built on Redis Enterprise. This post covers the selection, the deployment, and the patterns that separate a cache that helps from a cache that becomes your most exciting single point of failure.

Choosing a Tier

For new deployments, look at Azure Managed Redis first: it runs current Redis versions, multiplexes across cores properly, offers zone redundancy across all its tiers, and generally delivers better price performance than the classic Premium tier. Classic Basic and Standard remain fine for dev and modest production. Whatever you pick, the sizing driver is rarely memory alone: watch connection counts, network bandwidth caps per SKU, and server load percentage, because Redis is single threaded per shard and CPU saturation shows up as latency long before memory fills.

resource "azurerm_redis_cache" "this" {
  name                          = "redis-web-prod"
  location                      = "westeurope"
  resource_group_name           = azurerm_resource_group.data.name
  capacity                      = 1
  family                        = "P"
  sku_name                      = "Premium"
  minimum_tls_version           = "1.2"
  public_network_access_enabled = false
  redis_version                 = "6"
  zones                         = ["1", "2", "3"]

  redis_configuration {
    maxmemory_policy                        = "volatile-lru"
    active_directory_authentication_enabled = true
  }
}

resource "azurerm_private_endpoint" "redis" {
  name                = "pep-redis-web-prod"
  location            = "westeurope"
  resource_group_name = azurerm_resource_group.data.name
  subnet_id           = azurerm_subnet.endpoints.id

  private_service_connection {
    name                           = "psc-redis"
    private_connection_resource_id = azurerm_redis_cache.this.id
    subresource_names              = ["redisCache"]
    is_manual_connection           = false
  }
}

Notice Entra authentication enabled and public access off. Access keys in app settings are the Redis equivalent of SQL logins; managed identity auth removes them.

Cache Aside Done Right

The dominant pattern is cache aside: read the cache, on miss read the database and populate, on writes invalidate. The naive version has two production failure modes. First, cache stampede: a hot key expires and five hundred concurrent requests all hit the database together. Fix it with jittered TTLs so keys do not expire in synchronized waves, plus a short lock or in flight request coalescing so only one caller rebuilds a hot key. Second, stale data after failed invalidation: do the database write first, then the cache delete, and put a TTL on everything so no mistake outlives it.

public async Task<Product> GetProductAsync(string id)
{
    var key = $"product:{id}";
    var cached = await _redis.StringGetAsync(key);
    if (cached.HasValue)
        return JsonSerializer.Deserialize<Product>(cached);

    var product = await _db.Products.FindAsync(id);
    if (product is not null)
    {
        var ttl = TimeSpan.FromMinutes(30)
            + TimeSpan.FromSeconds(Random.Shared.Next(0, 120));
        await _redis.StringSetAsync(key,
            JsonSerializer.Serialize(product), ttl);
    }
    return product;
}

Connection Resilience

Most Redis incidents I have investigated were client side. The rules: one shared ConnectionMultiplexer per process, never per request; abortConnect set to false so the client retries instead of dying at startup; sensible connect and command timeouts; and an application that treats the cache as optional, degrading to the database instead of throwing when Redis is briefly unavailable during patching or failover. Azure patches nodes monthly, and a well configured client makes those events invisible.

Eviction and Monitoring

Pick the maxmemory policy deliberately. volatile-lru evicts only keys with TTLs, which pairs with the rule that everything gets a TTL. allkeys-lru is safer if teams forget TTLs. Never run noeviction for a cache workload unless you enjoy write errors at full memory. Monitor five numbers: cache hit ratio (below 85 percent means your TTLs or key design need work), server load, evicted keys per second, connected clients against the SKU limit, and network bandwidth against the cap. Alert on the trendline, not just thresholds, because caches degrade gradually and then all at once.

Cheers
Osama

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.