Best Practices for Redis Client Library Usage on Azure

Azure Cache for Redis can serve sub-millisecond responses at millions of operations per second. It can also become the bottleneck that your application blames on the database, the network, or the load balancer. Most Redis performance problems on Azure are not Redis problems. They are client configuration problems: too many connections being opened and closed, retry logic that amplifies load under pressure, timeouts set too low for what the operation actually requires.

This guide covers the client-side configuration that matters. The examples use StackExchange.Redis (the standard .NET client), but the principles apply across ioredis (Node.js), Jedis and Lettuce (Java), and redis-py (Python).

Use a single shared client instance

The most common Redis anti-pattern in applications is creating a new connection per request. A Redis connection has a setup cost. Under load, an application creating hundreds of new connections per second will saturate the connection limit of the Redis instance and degrade performance for every caller.

The fix is simple: create one ConnectionMultiplexer (in StackExchange.Redis) per application process, share it via dependency injection, and let it manage the connection pool internally.

// Do this ONCE at startup
var redis = ConnectionMultiplexer.Connect("your-cache.redis.cache.windows.net:6380,password=...,ssl=True,abortConnect=False");
services.AddSingleton<IConnectionMultiplexer>(redis);

// Inject and use throughout the application
public class CacheService
{
    private readonly IDatabase _db;

    public CacheService(IConnectionMultiplexer redis)
    {
        _db = redis.GetDatabase();
    }
}

The GetDatabase() call is cheap: it returns a lightweight handle, not a new connection. The ConnectionMultiplexer manages multiple physical connections to the Redis server internally. For high-throughput scenarios, you can increase the number of physical connections by adjusting the connectRetry and connection multiplexing configuration, but the single-instance pattern is the prerequisite.

For Java, Lettuce connection pooling works similarly: create a RedisClient or RedisClusterClient once and share the connection or connection pool across threads. Jedis requires explicit connection pool configuration via JedisPool.

For Node.js with ioredis:

const Redis = require('ioredis');
const redis = new Redis({
  host: 'your-cache.redis.cache.windows.net',
  port: 6380,
  password: 'your-password',
  tls: { servername: 'your-cache.redis.cache.windows.net' },
  enableAutoPipelining: true,
});
// Export and reuse this instance
module.exports = redis;

Always enable TLS on Azure Cache for Redis

Azure Cache for Redis supports both non-TLS (port 6379) and TLS (port 6380). Non-TLS access is disabled by default on new caches; if you have enabled it for legacy reasons, disable it. TLS protects credentials and data in transit from network interception.

The connection string format for TLS with StackExchange.Redis:

your-cache.redis.cache.windows.net:6380,password=ACCESS_KEY,ssl=True,abortConnect=False

abortConnect=False is important: it tells the client not to abort immediately on a connection failure at startup, which is safer for applications that start before the Redis cache is fully available (common in container orchestration scenarios).

Configure authentication with Microsoft Entra ID

Rather than using the Redis access key (a static secret that must be rotated manually and stored securely), Azure Cache for Redis supports Microsoft Entra ID authentication for caches running Redis 6 or later (Premium tier and above).

With Entra ID authentication, your application authenticates using a managed identity. No password in the connection string, no secret rotation, automatic credential refresh.

var configOptions = new ConfigurationOptions
{
    EndPoints = { "your-cache.redis.cache.windows.net:6380" },
    Ssl = true,
    AbortOnConnectFail = false,
};
await configOptions.ConfigureForAzureWithTokenCredentialAsync(
    new DefaultAzureCredential());
var redis = ConnectionMultiplexer.Connect(configOptions);

For applications already using managed identities for other Azure services (Key Vault, Storage, SQL), this is the most secure and operationally clean approach.

Implement retry logic correctly

Redis operations fail occasionally. Network interruptions, brief cache unavailability during maintenance or scaling, transient failures under load: these are facts of distributed systems. Your application needs to handle them.

StackExchange.Redis handles reconnection automatically at the connection level. For operation-level retries, implement a retry policy in your application code. The Polly library is the standard for .NET:

var retryPolicy = Policy
    .Handle<RedisConnectionException>()
    .Or<RedisTimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(Math.Pow(2, attempt) * 100),
        onRetry: (exception, timeSpan, retryCount, context) =>
        {
            logger.LogWarning($"Redis retry {retryCount} after {timeSpan.TotalMilliseconds}ms");
        });

var value = await retryPolicy.ExecuteAsync(async () => 
    await _db.StringGetAsync("mykey"));

The exponential backoff (100ms, 200ms, 400ms) is critical. Linear retries that immediately retry failures amplify load on a cache that is already under pressure. Add jitter (a small random offset to each backoff interval) if multiple instances are retrying simultaneously, to spread the retry load.

What not to retry: Operations that modify data (writes) should be retried only if they are idempotent or if you are confident the first attempt failed before execution (e.g., a connection exception before the command was sent, not after). Blindly retrying a non-idempotent write can result in duplicate operations.

Set timeouts that match your operation expectations

The default synchronous timeout in StackExchange.Redis is 5 seconds. The default async timeout is also 5 seconds. These are often too high for cache operations: if a Redis GET takes 5 seconds to respond, something is seriously wrong and you should fail fast rather than holding a thread for 5 seconds.

Conversely, timeouts set too low generate false positives during momentary latency spikes.

A practical starting point:

var configOptions = new ConfigurationOptions
{
    EndPoints = { "your-cache.redis.cache.windows.net:6380" },
    ConnectTimeout = 5000,   // 5 seconds for initial connection
    SyncTimeout = 1000,      // 1 second for synchronous operations
    AsyncTimeout = 1000,     // 1 second for async operations
    AbortOnConnectFail = false,
    Ssl = true,
};

Tune these based on observed p99 latency from your monitoring. If your 99th percentile Redis operation takes 50ms, a 200ms timeout gives a reasonable buffer without holding resources for seconds on genuine failures.

Use pipelining for bulk operations

Pipelining sends multiple commands to Redis in a single round trip. For operations that do not depend on each other's results, pipelining reduces latency significantly in high-latency network conditions (including cross-region calls).

StackExchange.Redis pipelines automatically when multiple async operations are in-flight on the same connection. To take advantage, issue async calls without awaiting each one before issuing the next:

var db = redis.GetDatabase();
var batch = db.CreateBatch();
var task1 = batch.StringGetAsync("key1");
var task2 = batch.StringGetAsync("key2");
var task3 = batch.StringGetAsync("key3");
batch.Execute();
await Task.WhenAll(task1, task2, task3);

ioredis supports automatic pipelining via enableAutoPipelining: true in the client configuration. Jedis requires explicit pipeline batches. Lettuce pipelines automatically for async operations.

Monitor cache metrics

Monitoring Redis properly closes the feedback loop that makes all of the above tuning decisions defensible. The key metrics for Azure Cache for Redis via Azure Monitor:

  • Cache hits and misses: A falling hit rate indicates cache invalidation problems or key expiry misalignment.
  • Connected clients: Spikes in connected clients indicate the application is not reusing connections correctly.
  • Server load: Redis is single-threaded for command processing. Server load above 80% consistently indicates the cache needs scaling or commands need optimisation.
  • Used memory: Approaching the cache memory limit causes eviction of keys under LRU or other eviction policies, which can cause unexpected cache misses.
  • Errors per second: ConnectionException and TimeoutException counts indicate client configuration or network issues.

Expose these metrics in your observability platform alongside application-level cache hit rates and latency percentiles. A server-side metric (Azure Monitor) combined with a client-side metric (application timing of Redis operations) gives you both ends of the picture.

Where Critical Cloud comes in

Redis client configuration problems are easy to introduce and slow to notice, because they typically manifest under load rather than in functional testing. We operate Azure Cache for Redis as part of the managed platform for technology-led businesses, with Datadog monitoring covering both Azure Monitor metrics and application-level Redis instrumentation. As the world's first Powered by Datadog accredited partner, we catch connection pool exhaustion, timeout spikes, and eviction events as operational signals, not as post-incident analysis. See how Critical Support works.