← Back to all posts

Azure Managed Redis in .NET

Azure Managed Redis in .NET

Reading Time: 7 minutes

The Problem

A lot of applications are slower than they need to be because they keep asking the primary database the same question over and over again.

Typical examples look like this:

  • product details that barely change but are loaded on every request
  • user profile or session data fetched repeatedly across multiple page loads
  • expensive aggregates or dashboard results recalculated far too often
  • configuration data read from a durable store every time instead of being reused

The result is predictable:

  • higher latency for users
  • unnecessary load on the database
  • lower throughput during traffic spikes
  • more infrastructure cost just to serve repeated reads

At small scale, this is inefficient. At larger scale, it becomes one of the simplest ways to burn performance and money at the same time.

The Solution

Azure Managed Redis gives you a fast, in-memory data store that sits closer to the application than your primary database.

It is a good fit when you need:

  • low-latency key-value lookups
  • distributed caching across multiple app instances
  • shared session or output cache state
  • temporary storage for precomputed results, counters, or rate limits
  • Redis features such as expirations, sets, sorted sets, and atomic increments

For most .NET applications, the default architectural pattern is cache-aside:

  1. Try the cache first.
  2. If the key is present, return it immediately.
  3. If the key is missing, load the data from the primary store.
  4. Put the result into Redis with an expiration.
  5. Return the data.

That keeps Redis fast, simple, and disposable. The primary database stays authoritative. Redis stays an optimization layer.

Description

Use Azure Managed Redis, not legacy Azure Cache for Redis for new work

If you are starting now, target Azure Managed Redis.

Microsoft guidance is already pointing new work there, and the older Azure Cache for Redis service has a retirement path. That matters because you do not want to build a new integration on a platform you will soon have to migrate away from.

What Redis is good at

Redis is not a replacement for your transactional database. It is a high-speed memory store that is especially useful for:

  • caching read-heavy data
  • session state shared across app instances
  • output caching in web applications
  • short-lived tokens or temporary state
  • counters, leaderboards, and throttling primitives

The common mistake is trying to treat Redis as the source of truth for business data. That usually creates durability and consistency problems you never needed in the first place.

Latest .NET package versions used in the examples

The main sample below uses these package versions:

  • Microsoft.Azure.StackExchangeRedis 3.3.1
  • StackExchange.Redis 2.12.14
  • Azure.Identity 1.21.0

Install them with:

dotnet add package Microsoft.Azure.StackExchangeRedis --version 3.3.1
dotnet add package StackExchange.Redis --version 2.12.14
dotnet add package Azure.Identity --version 1.21.0

If you want the simpler ASP.NET Core distributed cache abstraction instead of direct Redis commands, the current package is:

  • Microsoft.Extensions.Caching.StackExchangeRedis 10.0.5
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 10.0.5

Prefer passwordless authentication

For Azure-hosted applications, prefer Microsoft Entra ID authentication over access keys.

That gives you:

  • no Redis secrets in application configuration
  • managed identity support in Azure
  • better alignment with least-privilege access
  • easier rotation because there is no shared password embedded in every app

The official .NET guidance for Azure Managed Redis uses DefaultAzureCredential together with Microsoft.Azure.StackExchangeRedis to configure a StackExchange.Redis connection.

Configuration

Store only the Redis endpoint in configuration:

{
  "Redis": {
    "Endpoint": "your-cache-name.eastus.redis.azure.net:10000"
  }
}

Options model

public sealed class RedisOptions
{
    public const string SectionName = "Redis";

    public string Endpoint { get; set; } = string.Empty;
}

Create a singleton Redis connection

ConnectionMultiplexer is expensive to create and is designed to be reused. Do not create a new connection per request.

using Azure.Identity;
using Microsoft.Azure.StackExchangeRedis;
using Microsoft.Extensions.Options;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddOptions<RedisOptions>()
    .Bind(builder.Configuration.GetSection(RedisOptions.SectionName));

builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var options = sp.GetRequiredService<IOptions<RedisOptions>>().Value;

    var configuration = ConfigurationOptions.Parse(options.Endpoint);
    configuration.AbortOnConnectFail = false;

    configuration
        .ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential())
        .GetAwaiter()
        .GetResult();

    return ConnectionMultiplexer.Connect(configuration);
});

builder.Services.AddSingleton(sp =>
    sp.GetRequiredService<IConnectionMultiplexer>().GetDatabase());

builder.Services.AddSingleton<ProductCache>();

There are three important choices here:

  • AbortOnConnectFail = false is generally the safer production default because transient startup failures do happen
  • the connection is a singleton because Redis connections are meant to live for the lifetime of the process
  • the application resolves IDatabase once through DI because that keeps calling code cleaner when you only need a single logical database

GetDatabase() itself is not expensive. StackExchange.Redis treats it as a cheap pass-through and internally caches common database instances. So calling _redis.GetDatabase() on demand is valid. The update above is mainly about cleaner application code, not fixing a performance problem.

Cache-aside in practice

This is the pattern most teams actually need.

using System.Text.Json;
using StackExchange.Redis;

public sealed record ProductDto(Guid Id, string Name, decimal Price);

public interface IProductRepository
{
    Task<ProductDto?> GetByIdAsync(Guid productId, CancellationToken cancellationToken = default);
}

public sealed class ProductCache
{
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);

    private readonly IDatabase _database;
    private readonly IProductRepository _repository;

    public ProductCache(
        IDatabase database,
        IProductRepository repository)
    {
        _database = database;
        _repository = repository;
    }

    public async Task<ProductDto?> GetAsync(
        Guid productId,
        CancellationToken cancellationToken = default)
    {
        var cacheKey = $"products:{productId}";

        var cachedValue = await _database.StringGetAsync(cacheKey);
        if (cachedValue.HasValue)
        {
            return JsonSerializer.Deserialize<ProductDto>(cachedValue!);
        }

        var product = await _repository.GetByIdAsync(productId, cancellationToken);
        if (product is null)
        {
            return null;
        }

        var serialized = JsonSerializer.Serialize(product);

        await _database.StringSetAsync(cacheKey, serialized, CacheDuration);

        return product;
    }

    public async Task InvalidateAsync(Guid productId)
    {
        await _database.KeyDeleteAsync($"products:{productId}");
    }
}

This is intentionally boring. That is a good thing.

If you prefer, you can still inject IConnectionMultiplexer into a lower-level Redis service and call GetDatabase() there. That is also fine. The main reason to inject IDatabase directly is to avoid leaking Redis connection details into every service that just wants to execute commands.

Good Redis usage is usually simple:

  • consistent key naming
  • explicit TTLs
  • cache only data you can safely rebuild
  • invalidate on writes when the underlying data changes

Using Redis through IDistributedCache

If your application only needs a general-purpose distributed cache abstraction, ASP.NET Core can sit on top of the same Redis connection.

using Azure.Identity;
using Microsoft.Azure.StackExchangeRedis;
using StackExchange.Redis;

var builder = WebApplication.CreateBuilder(args);

var redisConfig = ConfigurationOptions.Parse(builder.Configuration["Redis:Endpoint"]!);
await redisConfig.ConfigureForAzureWithTokenCredentialAsync(new DefaultAzureCredential());

var redis = await ConnectionMultiplexer.ConnectAsync(redisConfig);

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.InstanceName = "app-cache:";
    options.ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(redis);
});

This is a good fit for:

  • session state
  • simple cached blobs
  • output caching support

It is less useful when you want direct access to Redis primitives like sets, hashes, sorted sets, or atomic counters. In those cases, use StackExchange.Redis directly.

As a rule of thumb:

  • use IDatabase when you want direct Redis commands with cleaner application code
  • use IConnectionMultiplexer when you need broader Redis capabilities such as pub/sub or server-level access
  • use IDistributedCache when Redis is only acting as a simple distributed cache store

Operational advice that matters

The most useful Redis advice is not about syntax. It is about boundaries.

  • keep the database as the source of truth
  • set expirations deliberately rather than caching forever
  • deploy the app and Redis in the same region when possible
  • expect failover and transient reconnects
  • instrument cache hit rate and latency instead of assuming the cache is helping
  • do not create massive values just because Redis is fast

Also remember that Redis is an in-memory system. Optional persistence improves recovery, but it is not a reason to treat the cache as your only copy of important data.

Summary

Azure Managed Redis is one of the easiest ways to reduce read latency and take pressure off your primary datastore, but only if you use it with discipline.

For .NET applications, the pragmatic defaults are:

  • use Azure Managed Redis for new work
  • authenticate with DefaultAzureCredential
  • reuse a singleton ConnectionMultiplexer
  • implement cache-aside instead of treating Redis as the system of record
  • use IDistributedCache only when the simpler abstraction is enough

That gives you a cache that is fast, secure, and operationally predictable instead of one more moving part that quietly becomes critical infrastructure.

References