See It Flow
Interactive guide · 8 chapters · ~30 min read

Production Engineering

maxmemory-policy left at its default, no appendonly, and client timeouts that assume Redis never blocks — these are the gaps between a quick-start Redis instance and one that survives a real eviction event or a slow KEYS call in production. This section covers the configs, persistence trade-offs, security controls, and monitoring that close them.

Mangesh MandlikLast updated:
01 · Where Redis fits

Production Use Cases

Before you tune anything, know what you're building. Redis in production usually shows up in one of six shapes.

Cache-aside for a database

The classic. Application checks Redis first; on miss, queries the database and populates Redis with a TTL. Shape: high read-to-write ratio, tolerable staleness. Watch out for: thundering herds when hot keys expire simultaneously — mitigate with jittered TTLs and single-flight fetching.

Session store

One key per session, hash value with user id, permissions, and last-touched timestamp. Shape: many small keys with per-key TTL. Watch out for: if you use replication for failover, expect to lose up to a few seconds of session writes — plan for graceful re-login or use a replicated persistence layer.

Rate limiting

One counter per (user, window). INCR user:42:m1 + EXPIRE user:42:m1 60. Reject when the counter exceeds the threshold. Shape: billions of tiny keys, most expiring in seconds. Watch out for: counter keys should never bear precious data — they can be lost freely.

Distributed locks

SET lock:order:42 owner NX PX 5000 for a 5-second lock. Release with a Lua-scripted compare-and-delete. Shape: coordination across microservices. Watch out for: clock drift, process pauses, and network partitions — the well-known 'Redlock' debate (Kleppmann vs antirez) lives here. For correctness-critical or financial work, use fencing tokens and/or an idempotency key at the target service; a Redis lock alone is not enough.

Real-time counters and leaderboards

ZINCRBY leaderboard 1 alice + ZRANGE leaderboard 0 9 REV WITHSCORES. Shape: high write throughput on a small number of hot keys. Watch out for: hot-key contention — a single sorted set can become the bottleneck of the whole cluster. Consider sharding leaderboards by region or time bucket.

Message queue / event stream

For simple queues: LPUSH work + BRPOP work or LMOVE work processing RIGHT LEFT (the replacement for the deprecated RPOPLPUSH) for at-least-once. For real event processing: Streams with consumer groups. Watch out for: unbounded queue growth if consumers fall behind — always have a length-based alert.

02 · Configs that matter

Configuration

Redis has 200+ config directives. Ten of them decide whether your production instance is fast, durable, and predictable. Learn these; the rest are noise until you have a specific problem.

confA representative production configuration for a cache-style instance; choose maxmemory-policy to match the instance's role
# Memory
maxmemory 8gb
maxmemory-policy allkeys-lru

# Persistence
save 900 1
save 300 10
appendonly yes
appendfsync everysec

# Networking
timeout 0
tcp-keepalive 300

# Client limits
maxclients 10000

# Slow log
slowlog-log-slower-than 10000
slowlog-max-len 256

# Optional defense-in-depth for dangerous commands
rename-command FLUSHALL ""
rename-command KEYS ""
rename-command CONFIG "CONFIG_a1b2c3"

maxmemory + maxmemory-policy

Never run Redis without a maxmemory limit. If you don't set one, Redis will happily use all available RAM and get OOM-killed by the kernel — taking your cache offline. Set an explicit limit for the instance's role, and leave headroom for allocator / fragmentation overhead, replication and client output buffers, and fork() copy-on-write growth during persistence.

The policy decides what happens when you hit the limit. allkeys-lru evicts across all keys and can fit a disposable cache when recency is a good proxy for value. volatile-lru only evicts keys with a TTL — useful when you mix cache and persistent data in the same instance. noeviction returns errors on write instead of evicting — the correct choice when Redis is a source of truth, not a cache.

Persistence trio

save lines configure RDB snapshot triggers ("if N keys changed in M seconds"). appendonly yes turns on AOF. appendfsync everysec typically limits acknowledged-write loss to about a second, but it's a durability/latency trade-off rather than an unconditional guarantee. RDB + AOF is a reasonable choice for stateful or durability-sensitive roles; a pure cache may choose lighter persistence or none at all.

Dangerous commands

FLUSHALL wipes the entire dataset. KEYS * blocks the server for as long as it takes to scan every key and can create a meaningful stall on a large keyspace. CONFIG SET can change persistence, security, and memory limits at runtime. If your ops model allows it, disable or rename these as a defense-in-depth guardrail — but treat ACLs, authentication, and network isolation as the primary safety controls.

Security basics

Never expose Redis directly to the public internet. Put it on a private network, restrict which application nodes can reach it, and keep routine app access separate from administrative access.

Require authentication and ACLs. Give application clients the smallest command and key-pattern permissions they need, keep admin identities separate, and store Redis secrets in your normal secret-management path rather than config files or shell history.

Enable TLS anywhere Redis traffic crosses untrusted networks or shared infrastructure. Combine that with explicit control over dangerous commands (CONFIG, FLUSH*, DEBUG, KEYS) so emergency and debugging access is intentional instead of available to every client.

03 · Making it fast

Performance

Redis is fast by default. "Redis is slow" is almost always a client-side or workload problem, not a Redis problem.

The three real bottlenecks

Slow commands. KEYS *, unbounded SMEMBERS, LRANGE 0 -1 on a million-item list, SORT on a big set — any command whose complexity is O(N) with a big N will block the single-threaded main loop. Everyone waiting is now stuck behind that one command. Use SCAN, bounded LRANGE, or model the data differently.

Big values. A single 100 MB string takes real time to serialize to the network. Redis P99 gets worse whenever any request touches a value that's much larger than the median. Keep values small — under a few hundred KB — or move the payload elsewhere and store only the reference.

Network round-trips. If your app does 100 sequential GETs to render a page, that's 100 network round-trips × 500µs each = 50 ms. The Redis server did 100µs of actual work. Pipeline those commands into one round-trip and the same page renders in 5 ms.

Pipelining and MULTI

pythonThe single most impactful client-side change for a high-traffic Redis workload
# Slow — 100 round trips
for k in keys:
    r.get(k)

# Fast — 1 round trip
pipe = r.pipeline()
for k in keys:
    pipe.get(k)
results = pipe.execute()

MULTI / EXEC wraps a group of commands in a transaction — they run atomically as one unit on the server. Combine with WATCH for optimistic concurrency. Note that Redis transactions are not rolled back on individual command errors; they're atomic in the sense of "executed together with no interleaving", not "all-or-nothing on error".

Connection pooling

Opening a TCP connection to Redis takes far longer than serving a command. Use a client-side connection pool (every serious client library has one) and size it for your real concurrent usage. Too small and requests queue or time out; too large and you add connection overhead plus client-side head-of-line effects without creating more Redis capacity.

04 · Growing up

Scaling

Redis does not have one universal scaling ladder. Pick the next topology based on which limit you are actually hitting.

When one bigger box is still the right answer

Buy a bigger box. Command execution is serialized through Redis's main thread, so faster cores beat more cores until you enable I/O threads for network-bound workloads. RAM is the other dimension — you can only cache what fits. A single well-sized modern instance can handle far more traffic than most teams expect before any sharding is needed; the actual ceiling depends on command mix, value sizes, and network conditions rather than a fixed number. Vertical scaling gets you further than you'd think.

When read pressure or HA is the next constraint

Add N read replicas. Route read traffic to replicas via a client-side load balancer; keep writes on the primary. This works when your workload is read-heavy (most caches, session stores, feature flags). Watch out for staleness — the tolerance for that varies by feature.

When blast radius and workload isolation matter

Split by domain. Sessions on Redis A, cache on Redis B, rate limits on Redis C. Each instance is smaller and independently managed. This is often a strong answer — it caps blast radius, lets each dataset have its own memory / persistence / policy, and doesn't require Cluster's constraints (no cross-slot commands, hash tags, etc.).

When one primary's write ceiling is the real limit

Horizontal write distribution. Data is split across N primaries using CRC16 slot hashing. Each primary has its own replicas. You gain more aggregate capacity, but you lose free-form multi-key operations — every command that touches multiple keys either uses hash tags to keep them on the same slot, or gets a CROSSSLOT error.

05 · Observability

Monitoring

Redis is stunningly stable when idle. Almost every incident is triggered by a specific operational condition. These are the metrics that catch each one.

Memory

`used_memory` and `used_memory_rss`. Watch the ratio — if RSS is far larger than used, investigate fragmentation, allocator behavior, and recent workload changes before reaching for activedefrag. `used_memory_peak` gives you sizing input. Alert on memory trend and remaining headroom relative to the instance's eviction tolerance, rather than one fixed percentage for every deployment.

Ops throughput

`instantaneous_ops_per_sec`. If this drops suddenly and CPU is idle, something's blocking the main loop. If this drops and CPU is 100%, you're at throughput ceiling — pipeline, add I/O threads, or shard.

Slow log

Enable slowlog-log-slower-than 10000 (10 ms). Run SLOWLOG GET 128 to see recent slow commands. This catches the single most common cause of unexplained latency: someone ran KEYS * or SMEMBERS bigset in a background job and stalled the main loop for a full second.

Blocked clients

`blocked_clients` metric. Legitimate blocking (BLPOP consumers) will show up here. But if this number grows unexpectedly, clients are stuck waiting on Redis responses — often a sign of network problems or a slow command in progress.

Replication lag

`master_repl_offset` on primary − `slave_repl_offset` on each replica. Growing lag means a replica isn't keeping up. Some lag is normal; lag that stays bounded is usually tolerable, but lag that grows without bound is a real problem. If a replica is far enough behind, it will trigger a full sync on reconnect — which is expensive and can pause replication for other replicas.

Evictions

`evicted_keys` counter. Increasing = you're at your memory limit and Redis is dropping data. For a cache this is fine (expected steady state). For a session store or lock store, this is a warning that maxmemory-policy is aggressive and you may lose non-cache data.

06 · Failure patterns

Common Production Failure Patterns

Four failure patterns that commonly affect Redis deployments, with their symptoms, causes and fixes. They are representative patterns, not reports of specific incidents.

The slow-command stall

Symptom: ops/s drops to zero for 2 seconds, everything unblocks, life continues. Cause: someone ran KEYS * from a debug console, or an app did SMEMBERS on a set that grew to a million members. Fix: SLOWLOG will show the culprit. Rename KEYS and FLUSHDB in production; replace SMEMBERS with SSCAN; monitor P99 command latency.

Fork-induced swap death

Symptom: the whole box grinds to a halt during a BGSAVE. Cause: Redis fork() to snapshot; if writes are heavy, copy-on-write doubles memory faster than expected; kernel starts swapping; the process dies. Fix: leave enough headroom for copy-on-write growth during persistence, set vm.overcommit_memory=1, disable Transparent Huge Pages (transparent_hugepage=never) — THP can make fork() and copy-on-write much slower.

The thundering herd

Symptom: a hot key expires; a thousand app servers all try to regenerate it at the same time; the database gets slammed; latency across the whole system spikes. Fix: jittered TTLs (ttl + random(0, 60s)), single-flight fetch coordination (one app server does the work, the rest wait on a Redis lock), and stale-while-revalidate patterns — serve the stale value while one worker refreshes.

Silent data loss on failover

Symptom: a payment was processed but the record disappeared after a failover. Cause: async replication — the primary crashed before the replica caught up. Sentinel promoted the replica, unaware of the last few writes. Fix: use WAIT n 5000 for financial or state-critical operations, or push idempotency guards to the target system (Postgres has INSERT ON CONFLICT, use it).

07 · Fixing it

Debugging Guide

The tools you'll actually reach for when Redis misbehaves.

MONITOR (use sparingly)

MONITOR streams every command being executed. Useful for a few seconds to see what's actually happening. Don't leave it running — it drops your instance throughput by ~50% because it duplicates every command to the monitor client.

SLOWLOG

SLOWLOG GET 128 returns the last 128 slow commands with timing and full argument list. This is the first tool to reach for on latency incidents. Reset with SLOWLOG RESET after triaging.

LATENCY monitor

CONFIG SET latency-monitor-threshold 100 to enable, then LATENCY LATEST to see the last big latency spike per source: fork, expire-cycle, command, AOF-fsync. This is how you distinguish 'a slow command' from 'the OS just paused us for a fork'.

MEMORY commands

MEMORY USAGE key returns the size of a value. MEMORY STATS gives a big breakdown of where memory is going. MEMORY DOCTOR runs heuristics and returns human-readable warnings. Use these when a Redis instance is unexpectedly bigger than it should be.

DEBUG SLEEP + DEBUG SEGFAULT

For testing your app's failure handling in staging. Never enable these in production without renaming. DEBUG SEGFAULT intentionally crashes the server so you can verify Sentinel failover actually works before you're relying on it in an incident.

08 · The rules

Best Practices

The habits that separate a Redis you love from a Redis you rebuild every quarter.

Set maxmemory, always

No exceptions. An unbounded Redis is a self-destruct waiting for load.

Never `KEYS *` in production

Use SCAN with a cursor for operational inspection; it can return duplicates and is not a point-in-time snapshot, so don't build correctness-critical logic on a single pass. KEYS is O(N) and blocks the main loop; disable or tightly restrict it in prod.

Pipeline aggressively

If your code does more than a few sequential Redis calls per request, pipeline them. The win is fewer network round-trips, not different server semantics.

Use TTLs on cache keys

Every cache key should have an EXPIRE. Adds a safety net for stale data and helps memory usage stay bounded.

Jitter TTLs

Add a random ±10% to expiration times. Prevents thundering herds when many keys were created at the same moment (e.g., during a warmup).

Split cache and coordination

One Redis instance for cache (where eviction is fine), another for locks / rate limits / sessions (where eviction would be a bug). Different maxmemory-policy settings.

Backup RDB off-cluster

Even if you run AOF, take periodic RDB snapshots and copy them off the Redis nodes. Ransomware, disk failure, or an ops mistake can wipe both primary and replica at once.

Test failover in staging

Run DEBUG SEGFAULT on a staging primary once a month. Watch Sentinel promote a replica. Watch your app reconnect. If any of these steps don't work smoothly, fix them before production teaches you.

Key takeaways
  • Cache-aside, sessions, rate limits, locks, counters, queues — a single Redis often plays 5 of these 6 roles.
  • Ten configs matter: maxmemory, maxmemory-policy, save, appendonly, appendfsync, tcp-keepalive, slowlog-log-slower-than, io-threads, rename-command KEYS, rename-command FLUSHALL.
  • Redis is slow almost always means slow commands (KEYS/SMEMBERS/big value) or missing pipelining — never Redis itself.
  • Scale by constraint: vertical for simple capacity, replicas for read-heavy or HA needs, functional sharding for blast-radius isolation, Cluster when one primary's write ceiling is the limit.
  • Alert on: memory trends and headroom, ops/s, P99 latency, blocked_clients, replication lag, evictions, slow log growth.
  • The four failure patterns: slow command stall, fork-induced swap, thundering herd, silent data loss on failover. Configure them out before they happen.