Redis System Design Guide: Caching, Eviction, Persistence & Distributed Locks
From Data Structure Internals to Redlock, Redis Cluster & Production-Grade Caching Architecture

What You Must Master to Clear This Track
- Know the exact complexity and internal encoding of Strings, Hashes, Sorted Sets, and Streams so you can justify data structure choices under load.
- Distinguish cache-aside, write-through, and write-behind by who owns writing to the cache and what staleness or data-loss window each accepts.
- Explain eviction policy trade-offs (LRU vs LFU vs TTL-based) and why the default noeviction policy silently rejects writes once maxmemory is hit.
- Articulate the RDB vs AOF durability trade-off: RDB is fast to restore but loses minutes of data, AOF with everysec loses at most one second but is slower to replay.
- Understand that Redis Cluster shards via 16384 hash slots (not consistent hashing) and that Redlock trades perfect safety for pragmatic multi-instance liveness.
Step-by-Step Study Plan
Follow this sequential roadmap designed to take you from core foundations to advanced architecture and mock interviews.
Data Structure Selection & Complexity Analysis
Strings, Hashes, Lists, Sets, Sorted Sets, and Streams, including their internal encodings (listpack/ziplist, skip list + hash table) and when to reach for each.
- •Explain why ZSET uses a skip list plus hash table to give O(log N) ZADD/ZRANGE and O(1) ZSCORE lookups simultaneously.
- •Model a leaderboard, a rate limiter, and a session store using the right primitive for each.
- •Understand Streams (XADD, consumer groups, XACK/XPENDING) as a lightweight Kafka alternative for at-least-once delivery.
- •Run MEMORY USAGE and OBJECT ENCODING on real keys to see listpack vs hashtable/skiplist transitions.
- •Practice explaining Big-O for at least 6 commands per data type out loud.
Cache Consistency Strategies & Durability Trade-offs
Cache-aside, write-through, and write-behind patterns; LRU/LFU/TTL eviction policies under memory pressure; RDB snapshotting vs AOF durability.
- •Draw the full cache-aside read/write path including cache stampede protection.
- •Compare maxmemory-policy options and pick the correct one for a session cache vs a computed-results cache.
- •Explain fork() + copy-on-write for BGSAVE and appendfsync always/everysec/no trade-offs for AOF.
- •Configure a local Redis instance with maxmemory and allkeys-lfu, then observe eviction under synthetic load.
- •Enable AOF with appendfsync everysec and inspect the rewrite log to understand compaction.
High Availability, Sharding & Redlock
Sentinel-based failover, Redis Cluster's 16384 hash slots, cross-slot operation limitations, Redlock distributed locking, and Redis-backed rate limiting.
- •Trace a Sentinel quorum failover from primary failure detection to replica promotion.
- •Explain hash slot routing, MOVED/ASK redirection, and resharding in Redis Cluster.
- •Implement a Redlock-style lock across 3-5 independent Redis instances with a majority quorum and safe release via Lua.
- •Practice deriving RPS, memory footprint, and shard count for a 50M-user session store on a whiteboard.
- •Discuss known Redlock criticisms (clock drift, GC pauses) and how fencing tokens mitigate them.
1. Redis Core Data Structures & Time Complexity
Redis is fundamentally an in-memory data structure server, not just a key-value cache. Picking the right structure for the access pattern is what separates a naive cache from a well-designed one.
O(1) GET/SET/INCR. Backed by Simple Dynamic Strings (SDS); small integers use a shared object pool. Ideal for counters, feature flags, and serialized cache blobs (JSON/Protobuf).
O(1) HGET/HSET/HDEL. Encoded as a compact listpack for small field counts, converting to a full hash table past hash-max-listpack-entries. Ideal for storing an object's fields (a user profile) without a full JSON deserialize.
O(log N) ZADD/ZRANGE/ZRANK via a skip list combined with a hash table for O(1) score lookups. Ideal for leaderboards, priority queues, and sliding-window rate limiters keyed by timestamp score.
O(1) amortized XADD append-only log with consumer groups, XACK, and XPENDING for at-least-once delivery. Ideal for event sourcing, activity feeds, and lightweight pub/sub replacements that need replay and backpressure.
Lists (O(1) LPUSH/RPUSH, O(N) LINDEX) work well as bounded queues; Sets (O(1) SADD/SISMEMBER) are ideal for deduplication, tagging, and fast membership checks like unique-visitor tracking.
- Run OBJECT ENCODING <key> in interviews-style discussion to show you understand that small collections silently use compact encodings (listpack) before promoting to hash tables or skip lists.
- Prefer Hashes over storing one String key per object field — it collapses N keys into 1, cutting memory overhead from per-key bookkeeping.
- Using a single giant String (a multi-MB JSON blob) as a hot key — Redis is single-threaded per command, so large value serialization/deserialization blocks the event loop.
- Choosing a List for a queue that needs random access or dedup — Lists have O(N) LINDEX/LREM, which quietly becomes a bottleneck as it grows.
2. Caching Patterns: Cache-Aside, Write-Through, Write-Behind & Eviction
The caching pattern you choose determines who is responsible for cache population, how stale reads can get, and what happens to writes during a cache outage.
Application code checks the cache first; on a miss it reads the DB and populates the cache with a TTL. Simplest and most common pattern — cache only holds what's actually requested, but the first request after a miss always pays the full DB latency.
Every write goes through the cache, which synchronously writes to the DB before acknowledging. Guarantees cache and DB never diverge, at the cost of added write latency on every request.
Writes land in the cache and are asynchronously flushed to the DB in batches. Lowest write latency and highest throughput, but risks data loss if the cache crashes before the flush completes.
allkeys-lru/allkeys-lfu approximate true LRU/LFU using random sampling (not a full linked list) for O(1)-ish eviction; volatile-ttl evicts keys closest to expiry first; noeviction (the default) rejects writes outright once maxmemory is reached.
The request path an application follows on a cache miss versus a subsequent cache hit.
import { redis } from "./redisClient";
import { db } from "./db";
const TTL_SECONDS = 300;
const JITTER_SECONDS = 30;
async function getProduct(productId: string) {
const cacheKey = `product:${productId}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// Prevent cache stampede: only one caller rebuilds a hot key
const lockKey = `lock:${cacheKey}`;
const acquiredLock = await redis.set(lockKey, "1", "NX", "EX", 5);
if (!acquiredLock) {
// Another request is already rebuilding; brief backoff then retry read
await new Promise((r) => setTimeout(r, 50));
return getProduct(productId);
}
try {
const product = await db.products.findById(productId);
if (product) {
const ttl = TTL_SECONDS + Math.floor(Math.random() * JITTER_SECONDS);
await redis.set(cacheKey, JSON.stringify(product), "EX", ttl);
}
return product;
} finally {
await redis.del(lockKey);
}
}- For write-heavy workloads with tolerable data-loss windows (e.g. view counters, analytics), write-behind batching to the DB can cut write amplification by an order of magnitude.
- Always set an explicit maxmemory-policy — leaving the default noeviction on a pure cache instance means Redis starts returning OOM errors on writes instead of evicting old data.
- Forgetting to invalidate or update the cache key on a write path in cache-aside, leaving stale data served until TTL expiry.
- Using write-through for a cold, rarely-read dataset — you pay synchronous write cost for data that may never be read from cache again.
3. Persistence: RDB Snapshots vs AOF & Durability Trade-offs
Redis is in-memory first, but production deployments almost always need a durability story for restarts and crash recovery — the choice between RDB and AOF is a direct trade-off between recovery speed and data-loss window.
SAVE/BGSAVE forks the process and uses copy-on-write to write a compact binary snapshot without blocking the main event loop. Fast to load on restart, but any writes since the last snapshot (seconds to minutes) are lost on a crash.
Logs every write command; appendfsync controls durability: always (fsync every write, safest, slowest), everysec (fsync once per second, default, loses at most ~1s), no (OS-controlled, fastest, least safe).
Over time the AOF log grows unbounded; BGREWRITEAOF forks and rewrites it as the minimal set of commands needed to reconstruct current state, similar in mechanism to RDB's copy-on-write fork.
Redis 4+ can prefix the AOF file with an RDB-formatted snapshot on rewrite, combining RDB's fast restart with AOF's minimal data-loss window — the recommended default for most production deployments.
- For a pure cache tier where the DB is the source of truth, disable persistence entirely (save "", appendonly no) — losing the cache is cheap, and you avoid fork-related latency spikes on a large dataset.
- For Redis used as a system of record (queues, session stores without a backing DB), enable hybrid persistence with appendfsync everysec as the default balance.
- Running BGSAVE on a host with insufficient free memory — copy-on-write can double memory usage under heavy write load during the fork, triggering OOM kills.
- Assuming appendfsync always is 'safe' without measuring latency impact — it fsyncs on every single write and can drop throughput by 10x or more on spinning or network-attached disks.
4. Replication, High Availability, Distributed Locks & Rate Limiting
Beyond a single node, production Redis needs a failover story (Sentinel), a sharding story (Cluster), and often distributed coordination primitives (Redlock) layered on top.
Primary replicates to replicas asynchronously by default; replicas can serve reads but may lag behind, so a failover can lose the last few unacknowledged writes (an AP-leaning trade-off, not full CP consistency).
A quorum of independent Sentinel processes monitors primaries, agrees via majority vote that a primary is down, and promotes the most up-to-date replica — client libraries discover the new primary via Sentinel's pub/sub notifications.
Data is partitioned across exactly 16384 hash slots (CRC16(key) mod 16384), each owned by one primary node; multi-key operations across slots fail with a CROSSSLOT error unless keys share a hash tag ({user123}:profile).
Acquire a lock with the same key/value across N independent Redis masters (typically 5); the lock is considered held only if a majority (N/2+1) acknowledge within a bounded time budget, and release deletes the key only if the value still matches (via a Lua script) to avoid deleting someone else's lock.
Token bucket via INCR + EXPIRE for fixed windows, or a sliding-window log using a ZSET keyed by timestamp (ZADD + ZREMRANGEBYSCORE to evict old entries + ZCARD to count) for smoother, burst-resistant limiting.
How a client request is routed to the correct shard using CRC16 hash slots, including redirection on resharding.
import { randomUUID } from "crypto";
import type Redis from "ioredis";
const LOCK_TTL_MS = 10_000;
const DRIFT_FACTOR = 0.01;
// Lua script: only delete the key if the value still matches our token
const RELEASE_SCRIPT = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`;
async function acquireLock(clients: Redis[], resource: string) {
const token = randomUUID();
const quorum = Math.floor(clients.length / 2) + 1;
const start = Date.now();
let acquiredCount = 0;
await Promise.all(
clients.map(async (client) => {
const ok = await client.set(resource, token, "PX", LOCK_TTL_MS, "NX");
if (ok === "OK") acquiredCount++;
})
);
const elapsed = Date.now() - start;
const validityMs = LOCK_TTL_MS - elapsed - Math.round(LOCK_TTL_MS * DRIFT_FACTOR);
if (acquiredCount >= quorum && validityMs > 0) {
return { token, validityMs };
}
// Failed to reach quorum: release on every node before giving up
await releaseLock(clients, resource, token);
return null;
}
async function releaseLock(clients: Redis[], resource: string, token: string) {
await Promise.all(clients.map((client) => client.eval(RELEASE_SCRIPT, 1, resource, token)));
}- Use hash tags like {order:5001}:items and {order:5001}:status to force related keys into the same slot so multi-key operations (MGET, transactions) don't hit CROSSSLOT errors.
- For most single-datacenter use cases, a well-tuned Sentinel setup with WAIT for replica acknowledgment is simpler and sufficient — reach for Redlock only when you need cross-instance mutual exclusion, not just single-primary locking.
- Treating Redlock as a perfect distributed consensus mechanism — under process pauses (GC, VM stop-the-world) or clock jumps, two clients can briefly believe they both hold the lock; use fencing tokens on the protected resource for true safety.
- Running Redis Cluster with only 3 nodes and no replicas — losing a single primary before its slot range is recovered causes cluster-wide unavailability for that slot range.
Eliminating Overselling and Database Overload During Flash Sales at a Mid-Size E-Commerce Platform
A mid-size e-commerce company's flagship flash-sale events routinely overwhelmed their primary MySQL read replicas, causing checkout latency spikes above 4 seconds and, worse, occasional overselling of limited-inventory SKUs when multiple app instances decremented stock concurrently without coordination.
- 1Introduced a cache-aside layer in front of product catalog reads using Redis, with a 30-second TTL plus randomized jitter to prevent synchronized mass expiry across thousands of SKU keys.
- 2Added a short-lived per-key mutex (SET NX PX) so that on a cache miss for a suddenly hot product, only one request rebuilt the cache entry while concurrent requests waited briefly and retried, eliminating stampede-driven DB spikes.
- 3Replaced ad-hoc application-level inventory checks with a Redlock-based distributed lock across a 5-node Redis quorum to serialize stock-decrement operations per SKU across all app server instances.
- 4Migrated session storage from sticky in-memory sessions to a Redis Cluster deployment sharded via hash slots, enabling horizontal scaling of app servers without session-affinity constraints.
- 5Configured Sentinel-based automatic failover for the lock and session Redis tiers, with WAIT-based replica acknowledgment on critical inventory-lock writes to reduce the risk of losing a lock state during a primary failover mid-sale.
Top Must-Know Interview Questions & Model Answers
Q1: Why does Redis use a skip list combined with a hash table for Sorted Sets instead of just a balanced tree?
- •A skip list is simpler to implement correctly for range queries and concurrent-friendly traversal than a balanced tree, with comparable O(log N) average-case performance.
- •The parallel hash table maps member -> score directly, so checking or updating an individual member's score doesn't require walking the skip list at all.
- •This dual structure is why ZADD updates both structures atomically and why memory overhead for ZSETs is higher than for a plain hash.
Q2: How would you model a real-time leaderboard for 10 million users using Redis?
- •ZADD is O(log N), so updating a score on every game event scales well even at high write volume.
- •ZREVRANGE withscores 0 99 fetches the top 100 in O(log N + M); ZREVRANK gives any user's position in O(log N) without scanning the whole set.
- •For multi-region leaderboards, shard by game/tournament ID so each ZSET stays bounded rather than one global set across all users.
Q3: What is the practical difference between using Redis Lists and Redis Streams as a message queue?
- •A List-based queue loses a message permanently once BRPOP returns it to a worker that then crashes before processing.
- •Streams retain entries until explicitly trimmed (XTRIM) and track per-consumer-group delivery, so a crashed worker's unacknowledged messages (XPENDING) can be claimed by another consumer (XCLAIM).
- •Streams are the closer analog to a lightweight Kafka partition; Lists are closer to a simple job queue with no replay guarantees.
Q4: Why should you avoid storing one very large String value (multiple megabytes) as a single Redis key?
- •Even 'simple' GET/SET on a large blob costs real wall-clock time proportional to its size for network I/O and memory copy.
- •Splitting large objects into a Hash of smaller fields, or chunking into multiple keys, keeps individual command latency low and predictable.
Q5: What is the HyperLogLog data structure in Redis, and when would you use it over a Set?
- •A Set storing every unique visitor ID grows linearly with cardinality and can consume gigabytes for hundreds of millions of unique elements.
- •PFADD/PFCOUNT trade exactness for bounded memory — ideal for 'unique daily visitors' or 'unique searches' metrics where an approximate count is acceptable.
Q6: Explain the cache-aside pattern and its main weakness during a cold start or cache flush.
- •This 'cache stampede' or 'thundering herd' scenario can take down an underprovisioned database that was only ever sized for cache-hit traffic.
- •Mitigations include per-key mutexes/locks during rebuild, staggered TTLs with jitter, and pre-warming the cache before a known traffic spike.
Q7: Compare write-through and write-behind caching in terms of consistency and failure risk.
- •Write-through is preferred when correctness matters more than write latency (financial ledgers, inventory counts).
- •Write-behind is preferred for high write-volume, loss-tolerant data (metrics, view counts, activity logs) where batching dramatically reduces DB write pressure.
- •A hybrid approach batches write-behind flushes but also persists an intent log (e.g. via Streams) so a crashed cache node's pending writes can be replayed.
Q8: When would you choose read-through caching over application-managed cache-aside?
- •Frameworks or caching proxies that support read-through remove repetitive 'check cache, else query DB, else populate' boilerplate from every service.
- •It's less flexible when different call sites need different TTLs, serialization formats, or fallback behavior for the same key.
Q9: How does Redis's default maxmemory-policy of noeviction behave once memory is full, and why is that dangerous?
- •noeviction is the safe-by-default choice for Redis used as a system of record (not just a cache), where silently losing data via eviction would be worse than rejecting writes.
- •For a pure cache use case, this default should almost always be overridden to an active eviction policy so the application never sees OOM errors from a full cache.
Q10: What is the difference between allkeys-lru and volatile-lru eviction policies?
- •volatile-lru is useful when a Redis instance mixes cache data (with TTLs) and persistent data (without TTLs, e.g. session tokens you never want auto-evicted) on the same instance.
- •If no keys have a TTL under a volatile-* policy, Redis behaves like noeviction and starts rejecting writes.
Q11: How does Redis approximate LRU eviction without maintaining a true, fully-ordered LRU linked list?
- •Increasing maxmemory-samples improves approximation accuracy at the cost of more CPU per eviction; Redis also maintains a small eviction pool to make repeated approximations converge closer to true LRU over time.
- •For most workloads, approximate LRU performs close enough to true LRU that the memory savings are worth the trade-off.
Q12: When would LFU (Least Frequently Used) eviction outperform LRU for a caching workload?
- •A classic example is a product catalog where a handful of best-sellers are read thousands of times per minute alongside long-tail products read once and never again — LRU can evict a best-seller simply because a cold item was accessed a moment later.
- •LFU tracks an approximate access-frequency counter per key (with probabilistic decay) so consistently popular keys survive longer.
Q13: What happens during Redis's BGSAVE, and why doesn't it block client commands?
- •Because fork() is fast (page tables are copied, not the data itself), the parent process resumes serving requests almost immediately after the fork.
- •If write volume during the snapshot is very high, copy-on-write can cause significant extra memory usage, potentially doubling resident memory on a worst-case, all-keys-modified workload.
Q14: Compare the three AOF appendfsync options and their durability/performance trade-offs.
- •everysec is the recommended default for most production workloads because it bounds data loss tightly while keeping throughput close to no-fsync performance.
- •always is typically reserved for financial or compliance-critical data where even a 1-second loss window is unacceptable, accepting the throughput hit.
Q15: Why would a team choose hybrid RDB+AOF persistence instead of just one mechanism?
- •Loading a pure AOF file means replaying potentially millions of individual commands, which is far slower than loading one compact RDB snapshot.
- •Hybrid mode loads the RDB preamble instantly, then replays only the small tail of commands written since the last rewrite.
Q16: What's the risk of enabling only RDB persistence with default snapshot intervals (e.g. save 900 1)?
- •Tuning tighter save intervals (e.g. save 60 1) reduces the loss window but increases fork frequency and associated CPU/memory overhead.
- •For any dataset where losing minutes of writes matters, AOF (or hybrid) is required alongside or instead of RDB alone.
Q17: How does Redis Sentinel decide a primary is actually down before triggering failover?
- •This quorum requirement prevents a single Sentinel's network blip from triggering an unnecessary failover.
- •Once ODOWN is reached, Sentinels run a leader-election (using Raft-like majority voting) to pick one Sentinel to execute the failover, which promotes the most up-to-date replica (based on replication offset).
Q18: In Redis Cluster, why are keys partitioned into exactly 16384 hash slots instead of using simple consistent hashing?
- •Each node tracks slot ownership as a compact bitmap; with more slots (say millions), that bitmap and its gossip-protocol propagation overhead would grow too large.
- •Clients cache a slot-to-node mapping and only need to update it on MOVED errors, avoiding a full rehash of the keyspace during scaling events.
Q19: What causes a CROSSSLOT error in Redis Cluster, and how do you avoid it for multi-key operations?
- •For example, {order:5001}:items and {order:5001}:status both hash only on 'order:5001', guaranteeing they land in the same slot and can be used together in MGET, transactions, or Lua scripts.
- •This is a deliberate design trade-off: Cluster sacrifices arbitrary multi-key atomicity for horizontal scalability.
Q20: Why is asynchronous replication in Redis considered an AP-leaning trade-off rather than strongly consistent?
- •The WAIT command can force the primary to block until N replicas acknowledge a write, trading availability/latency for stronger durability guarantees on a per-command basis.
- •Even with WAIT, Redis replication is not fully synchronous/linearizable by default — it's an opt-in tool for specific critical writes, not the default behavior.
Q21: How would you size and design Redis Cluster nodes for a 50 million session, multi-region user base?
- •For example, 50M sessions x 2KB average x 2 (primary+replica) is roughly 200GB total memory — divided across, say, 20 shards of ~10GB each keeps snapshot and failover times manageable.
- •For multi-region access, consider active-active patterns (CRDB-based Redis Enterprise or application-level regional sharding) since native OSS Cluster replication is single-region-primary by design.
Q22: What problem does the Redlock algorithm solve, and what is its core mechanism?
- •Each acquisition attempt uses SET key value NX PX <ttl> on every instance; the lock is considered held only if a majority (e.g. 3 of 5) respond OK before a computed time budget expires.
- •Release uses a Lua script that checks the stored value matches the client's unique token before deleting, so a client never releases a lock it doesn't currently own (e.g. after its TTL already expired and was re-acquired by someone else).
Q23: What is the main criticism of Redlock, and how do fencing tokens address it?
- •Without fencing, a paused client can 'wake up' after its lock expired, still believe it holds the lock, and write to the protected resource concurrently with the new lock holder.
- •With fencing, the protected resource (e.g. a storage service) rejects any write carrying a fencing token lower than the highest it has already seen, making the lock safe even if two clients briefly both think they hold it.
Q24: Why should a single-instance Redis lock (SET NX PX) be considered insufficient for critical distributed coordination?
- •This is precisely the scenario Redlock's multi-instance majority quorum is designed to reduce the probability of, though not eliminate entirely under worst-case timing.
- •For many practical use cases (e.g. deduplicating a background job within one datacenter), a single-instance lock with a reasonable TTL and idempotent downstream operations is an acceptable, simpler trade-off.
Q25: How would you implement a sliding-window rate limiter using a Redis Sorted Set?
- •This gives a true sliding window (not a fixed-bucket approximation), smoothing out the burst-at-boundary problem where a fixed window allows 2x the limit right at the window edge.
- •Wrap the remove+count+add sequence in a Lua script or MULTI/EXEC to keep it atomic across concurrent requests for the same client.
Q26: Compare token bucket and fixed-window counter approaches for Redis-based rate limiting.
- •Fixed-window is O(1) per request and trivial to implement, making it a reasonable default when exact burst control isn't critical.
- •Token bucket requires a small Lua script to atomically compute elapsed time, refill tokens, and decrement on each request, but better models real-world 'allow bursts, then throttle' traffic shaping needs.
Q27: Why are Redis transactions (MULTI/EXEC) not equivalent to full ACID database transactions?
- •Only syntax errors detected at queue time abort the whole transaction; a runtime error (e.g. wrong type operation) on one command simply fails that command while the rest proceed.
- •For true conditional atomicity (check-then-act across multiple keys), WATCH provides optimistic locking, aborting EXEC if a watched key changed since WATCH was called.
Q28: Why is Redis effectively single-threaded for command execution, and how does that affect design decisions?
- •Since Redis 6, I/O threading was added to parallelize reading/parsing client input and writing output, but command execution itself remains single-threaded.
- •This is why SCAN (cursor-based, incremental) replaces KEYS (blocking, O(N) full scan) as the production-safe way to iterate a large keyspace.
Q29: What is the N+1 cache invalidation problem, and how do you handle invalidating a cache entry that depends on multiple upstream tables?
- •Explicit invalidation (pub/sub 'entity changed' events triggering targeted DEL calls) gives fresher data but adds coupling between every writer and the cache layer.
- •A pattern used at scale is publishing domain change events (via Streams or a message bus) that a dedicated cache-invalidation consumer subscribes to, decoupling writers from cache-invalidation logic.
Q30: How do you decide the right TTL for a cached value in a system design interview?
- •Adding jitter to any TTL avoids mass simultaneous expiry (cache stampede) for keys written in bulk at the same time.
- •For data that must never be stale beyond a hard bound (e.g. account balance shown at checkout), prefer explicit invalidation on write over relying on TTL alone.
Mistakes That Sink Otherwise Strong Candidates
Why it happens: Teams provision Redis for caching without revisiting the default configuration, assuming Redis will 'just evict old stuff' automatically once memory fills up.
The fix: Explicitly set an eviction policy like allkeys-lru or allkeys-lfu for pure cache instances, reserving noeviction only for Redis deployments acting as a system of record.
Why it happens: KEYS is the first command developers learn for inspecting a keyspace, and it works fine on a small local dataset, masking its O(N) blocking behavior until the dataset grows in production.
The fix: Use SCAN with a cursor for any production keyspace iteration — it processes the keyspace incrementally without blocking other clients.
Why it happens: Redlock's design looks like a rigorous consensus algorithm, so teams assume it eliminates all race conditions without accounting for clock drift or process pauses.
The fix: Use Redlock for reducing the probability of concurrent access in practice, and add fencing tokens on the protected resource itself whenever correctness truly cannot tolerate a rare double-acquisition.
Why it happens: A batch cache-warming job or bulk cache population at startup writes many keys with the exact same TTL value, so they all expire simultaneously later.
The fix: Add a randomized offset (e.g. +/- 10-15% of the base TTL) to every cache write so expirations spread out over time instead of clustering.
Why it happens: It's the path of least resistance to just JSON.stringify() an entire nested object and cache it as one value.
The fix: Break large objects into Hashes with smaller fields, compress payloads, or store only the frequently-accessed subset in Redis while keeping the full object in the primary datastore.
Why it happens: Redis replication feels instantaneous in local development with negligible network latency, hiding the asynchronous replication lag that appears under real production load.
The fix: Read from the primary for any read-after-write consistency requirement, or use WAIT to confirm replica acknowledgment before returning success for critical writes.
Why it happens: Single-node Redis development never surfaces CROSSSLOT errors, so the issue only appears after deploying to a real Cluster topology.
The fix: Use hash tags ({entityId}:field) from the start of key-schema design for any keys that will ever need to be operated on together.
Why it happens: Teams pick the 'safest sounding' option without benchmarking, assuming Redis's in-memory speed will absorb the fsync cost.
The fix: Benchmark appendfsync everysec first (the recommended default) and only move to always for the specific keys/use cases where a 1-second loss window is genuinely unacceptable.
Quick-Reference Cheat Sheet
Recommended Practice Quizzes on QuizCluster
Test your retention and prepare for timed live coding and MCQ technical screening rounds:
SQL & NoSQL Engines
Compare relational, key-value, and document engines including Redis's caching and persistence trade-offs.
High-Level System Design (HLD)
Practice caching layer design, sharding strategies, and distributed coordination scenarios.
Frequently Asked Questions
Is Redis a database or just a cache?
Both, depending on configuration. With persistence disabled, Redis is commonly used as a pure cache in front of a source-of-truth database. With RDB/AOF persistence, replication, and Cluster enabled, Redis is frequently used as a primary data store for use cases like session storage, leaderboards, and real-time counters.
Can Redlock guarantee perfect mutual exclusion in all failure scenarios?
No. Redlock provides strong practical guarantees under normal operating conditions and node failures, but under extreme clock drift or long process pauses (GC, VM suspension), it cannot guarantee perfect safety. Pair it with fencing tokens on the protected resource if correctness must hold even in those edge cases.
How do you scale Redis beyond what a single node's memory can hold?
Use Redis Cluster to shard data across multiple primary nodes via hash slots, each with its own replicas for high availability. Design keys with hash tags for any operations that need multiple keys to co-locate on the same shard.
When should I choose Redis over Memcached for caching?
Choose Redis when you need richer data structures (Sorted Sets, Hashes, Streams), persistence, replication/HA, pub/sub, or built-in support for patterns like distributed locks and rate limiting. Choose Memcached for the simplest possible pure key-value cache with a slightly lower per-operation memory overhead at very high scale.