Redis
An in-memory, single-threaded data-structure store — the most versatile tool in an interview because one technology covers caching, locks, leaderboards, rate limits, geo, queues, and pub/sub.
Also worth naming: Valkey (open-source fork) · Amazon ElastiCache · Amazon MemoryDB (durable variant)
Redis is the Swiss-army knife you can actually go deep on: learn a handful of data structures and you can reason about its behaviour in a distributed system from first principles, instead of memorising a dozen narrow tools.
What it is
Redis is an in-memory data-structure store written in C. The two facts that explain almost everything about it: it keeps data in RAM, and it executes commands on a single thread. That makes it astonishingly fast (sub-millisecond, 100K+ ops/sec per node) and easy to reason about — there is no lock contention between commands because there is only one command running at a time.
Where most databases are a black box of query planners and optimisers, Redis is a transparent set of data structures you already know from coding: strings, hashes (dictionaries), lists, sets, sorted sets (priority queues), bitmaps, HyperLogLogs, geospatial indexes, and streams (append-only logs). Every value lives under a string key, and the way you choose keys is the way you shard and scale.
The interview superpower is versatility. Instead of naming a different specialist system for every need, you can say "a Redis sorted set for the leaderboard, a Redis SET NX for the lock, a Redis stream for the work queue" — one technology you can defend deeply across many problems. The one thing to keep front of mind: by default Redis trades durability for speed, so it is a cache and a coordination layer, not usually your system of record.
When to reach for it
Reach for this when…
- You need a cache in front of a slower store and want sub-millisecond reads
- You need a leaderboard, a rate limiter, a distributed lock, or "nearby" geo queries
- You want a lightweight work queue or pub/sub without standing up Kafka
- You need shared, low-latency state across a fleet of stateless services (sessions, counters, presence)
Not really this pattern when…
- The data is your source of truth and losing seconds of writes is unacceptable (use a durable DB, or Redis only with MemoryDB-style guarantees)
- The working set is far larger than RAM is affordable for (RAM is the cost ceiling)
- You need rich queries, joins, or strong multi-key transactions across a large dataset (that is Postgres)
- You need a high-throughput durable log with long retention and replay (that is Kafka)
How it works
Three mechanisms explain how Redis behaves under load:
1. Single-threaded command execution. One thread runs all commands to completion, one at a time. This is why every command is effectively atomic, why INCR needs no locks, and why a single O(N) command on a huge key (KEYS *, SMEMBERS on a million-element set) stalls everything behind it. The mental rule: keep individual commands cheap, and never run an unbounded command in production.
2. RAM is the store; persistence is a safety net, not a guarantee. Redis offers two persistence modes, often combined:
- RDB — point-in-time snapshots forked to disk every N seconds. Compact and fast to load, but a crash loses everything since the last snapshot.
- AOF — an append-only log of every write, replayed on restart. Closer to durable, but
fsync-on-every-write is slow, so the common setting (everysec) can still lose ~1 second of writes.
If you genuinely need durability, you say so explicitly and reach for AOF-always or a durable variant like Amazon MemoryDB — you do not pretend default Redis is a database.
3. Cluster = 16,384 hash slots, no cross-node magic. In cluster mode every key is hashed (CRC16(key) % 16384) to a slot, and each node owns a range of slots. The client caches the slot→node map and talks directly to the owning node; a moved slot triggers a MOVED redirect so the client refreshes its map. The crucial limitation: a single command must touch keys in one slot. Multi-key operations across slots are rejected, which is why you use hash tags (user:{123}:profile, user:{123}:cart share a slot) when related keys must be operated on together.
The client caches a slot→node map and connects directly to the node that owns a key. Each primary has a replica for failover. There is no shared state between nodes — how you structure keys is how you scale.
Replication is asynchronous by default: a primary acks the client before its replica has the write, so a failover can lose the last few writes. The WAIT command can block until N replicas ack if you need stronger guarantees, at a latency cost.
Performance envelope
Redis performance envelope — the numbers to quote.
| Dimension | Number | Why it matters |
|---|---|---|
| Throughput | ~100K–300K ops/sec per node | Single-threaded core; one big node goes a long way before you shard |
| Latency | Sub-millisecond (p50 ~0.1–0.5 ms) | In-memory; same-AZ network RTT usually dominates the number |
| Pipelining | 1M+ ops/sec | Batch many commands per round trip to amortise network RTT |
| Memory per node | Tens of GB practical | RAM is the cost and the scaling ceiling — size the working set deliberately |
| Persistence cost | AOF everysec ≈ ~1s data-loss window | Durability is a dial, not a default |
| Scaling unit | 16,384 hash slots across nodes | Keys map to slots; multi-key ops must share a slot |
Capabilities in interviews
Redis as a cache
The default deployment: a hash map in front of a slower store, with TTLs for eviction.
Cache an expensive value under a key with an expiry, so the store is only hit on a miss:
SET product:123 "{...json...}" EX 3600 # cache for 1 hour
GET product:123 # hit → sub-ms; miss → load from DB, SET, returnThe TTL doubles as the eviction mechanism (Redis guarantees you never read a key after its TTL) and the freshness bound. Pick an eviction policy (allkeys-lru is the usual cache choice) so Redis sheds cold keys when memory is full rather than refusing writes. Use a real data structure when it helps — a hash for a partial-update object, a sorted set if you also need ordering — instead of storing one opaque JSON blob.
Choose this variant when
- Read-heavy paths over a slower DB
- Session storage for stateless services
- Memoising expensive computed results
Redis as a distributed lock
SET NX with a TTL for mutual exclusion — plus a fencing token for safety.
The basic lock is one atomic command:
SET lock:order:42 <token> NX EX 30 # acquire iff absent, auto-expire in 30s
# ... do work ...
# release via a Lua script that deletes only if the value is still <token>The TTL stops a crashed holder from deadlocking the resource forever. But a plain Redis lock is not safe on its own: if the holder stalls (a GC pause) past the TTL, a second worker can acquire the lock and both write. The fix is a fencing token — a monotonically increasing number issued with each lock that the protected resource validates, rejecting any write carrying a stale token.
SET NX gives mutual exclusion with a TTL so a crashed holder cannot deadlock. The monotonic token is the safety net: the protected resource rejects any write carrying a token lower than the highest it has seen, so a stalled ex-holder cannot clobber the new one.
Say this out loud in an interview: "a Redis lock gives mutual exclusion and crash-safety via TTL, and I add a fencing token because a paused holder can otherwise clobber the new owner." If you need airtight consensus, that is a job for etcd/ZooKeeper, not Redis.
Choose this variant when
- Short-lived mutual exclusion (checkout hold, cron leader)
- Preventing duplicate work across a worker fleet
- When you already run Redis and the resource can validate a token
Redis for leaderboards & ranking
Sorted sets keep members ordered by score with O(log N) writes and top-K reads.
A sorted set is the canonical answer to "top N by score":
ZADD leaderboard 4820 "player:7" # O(log N) insert/update
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10, O(log N + K)
ZREVRANK leaderboard "player:7" # a player's rank, O(log N)Backed by a skip list, it stays sorted on every write, so there is no re-sort and no scan. This is the move when a SQL ORDER BY ... LIMIT would buckle under write volume — high-write counters and rankings are exactly where Redis shines and a disk-based DB struggles.
A sorted set keeps members ordered by score in a skip list. ZADD is O(log N); reading the top K with ZREVRANGE is O(log N + K). No re-sort, no scan.
Choose this variant when
- Leaderboards, trending lists, top-K by any score
- Most-liked / most-recent feeds capped to N entries
- Anywhere you need ordered data with frequent updates
Redis as a rate limiter
Atomic counters with expiry implement token-bucket and sliding-window limits in one or two commands.
A fixed-window limiter is two commands (made atomic with a Lua script in production):
INCR rl:user:42:1700000000 # per-second bucket key
EXPIRE rl:user:42:1700000000 1
# if INCR result > limit → reject (429)For a sliding window, store request timestamps in a sorted set and trim old entries with ZREMRANGEBYSCORE before counting — wrap it in Lua so the trim-and-count is atomic. Because Redis is single-threaded, the counter increments are race-free without explicit locks, which is exactly why it is the standard home for distributed rate limiting.
Choose this variant when
- Per-user / per-API-key request quotas
- Abuse protection at the gateway
- Any counter that must be shared across many app servers
Redis for proximity search
Native geospatial commands answer "what is near me" in microseconds.
Redis indexes lat/lng with geohashes under the hood and exposes them directly:
GEOADD drivers -122.41 37.77 "driver:9"
GEOSEARCH drivers FROMLONLAT -122.41 37.77 BYRADIUS 2 km ASCThis is the hot-path cache tier for ride-sharing and "nearby" features: a Redis GEO index sharded by city absorbs the high-frequency location updates and serves dispatch queries sub-millisecond, while a durable store (PostGIS) remains the source of truth to rebuild from.
Choose this variant when
- "Find nearby" hot paths (dispatch, store locator)
- High-frequency location updates that would hammer a disk index
- A cache tier over a PostGIS / spatial source of truth
Redis Streams & pub/sub
Streams are a durable, replayable log with consumer groups; pub/sub is fire-and-forget fan-out.
Pub/sub broadcasts a message to all current subscribers — at-most-once, nothing stored. Great for ephemeral real-time fan-out (presence, live notifications); a subscriber that is offline simply misses the message.
Streams are an append-only log with consumer groups, closer to a lightweight Kafka:
XADD jobs * type "resize" id 991 # append an entry
XREADGROUP GROUP workers w1 COUNT 1 STREAMS jobs > # claim the next entry
XACK jobs workers 1526984818136-0 # mark processedThe group tracks pending (delivered-but-unacked) entries, so if a worker dies you reclaim its messages with XCLAIM. Use streams when you need durable, replayable at-least-once delivery but do not want to operate Kafka; reach for Kafka when you need very high throughput, long retention, and many independent consumer groups.
XADD appends to an append-only log. A consumer group hands each entry to exactly one worker and tracks pending (un-acked) entries, so a crashed worker's messages can be reclaimed with XCLAIM.
Choose this variant when
- Lightweight work queues with retries
- Real-time fan-out where missing a message is fine (pub/sub)
- Event flows that do not justify standing up Kafka
Operating knobs
Persistence: RDB vs AOF (vs neither)
Pure cache → no persistence (fastest, just rewarm on restart). Need to survive a restart without a cold cache → RDB snapshots. Need minimal data loss → AOF with everysec (≈1s window) or always (durable but slow). State this explicitly; "Redis with AOF every-second" is a very different durability story from default Redis.
Eviction policy
When memory is full, Redis either rejects writes (noeviction) or sheds keys. For a cache use allkeys-lru (or allkeys-lfu for skewed popularity). For a store where every key matters, noeviction + alerting on memory is safer than silently dropping data.
Topology: single node, replica, or cluster
Single node is fine until you need HA or outgrow one box. Add a replica + Sentinel for automatic failover (HA without sharding). Move to Cluster (hash slots) only when one node's RAM or throughput is the bottleneck — and design keys with hash tags so related keys co-locate.
Key + memory design
Memory is the ceiling, so size it: estimate keys × (key + value + overhead). Avoid big keys (a single multi-GB hash or list is an O(N) landmine and a rebalancing nightmare). Always set TTLs on cache keys so memory self-cleans.
Versus the alternatives
Redis vs the obvious alternatives.
| Dimension | Redis | Memcached | In-process cache |
|---|---|---|---|
| Data model | Strings, hashes, lists, sets, sorted sets, streams, geo, HLL | Opaque strings/blobs only | Whatever your language offers |
| Throughput | 100K–300K ops/sec/node | ~1M ops/sec (simpler, multi-threaded) | Nanoseconds — no network hop |
| Persistence / HA | RDB + AOF, replicas, cluster, Sentinel | None — pure volatile cache | Dies with the process |
| Scaling | Cluster via hash slots | Client-side sharding | Per-process only (no sharing) |
| Best for | Versatile shared state + coordination | Massive, simple LRU cache | Tiny ultra-hot set per node |
Failure modes & gotchas
One key (a celebrity, a viral product) takes a disproportionate share of traffic and saturates the single node that owns its slot. Mitigations: an in-process client cache for the hottest keys, replicating the value across several keys and randomising reads, or read replicas scaled for the hot slot. Recognise it and propose a fix — that is the senior signal.
Default Redis can lose the last seconds of writes on a crash or failover (async replication + periodic persistence). Fine for a cache; dangerous if you quietly made it your system of record. If you need durability, say AOF-always or MemoryDB out loud — do not assume it.
KEYS *, SMEMBERS on a huge set, or FLUSHALL block the single thread and stall every other client. Use SCAN for iteration, keep collections bounded, and never run unbounded commands against production.
A single multi-GB value (an ever-growing list or hash) makes every operation on it expensive, skews memory across the cluster, and turns resharding into a stall. Cap collection sizes (trim lists, expire members) and shard large logical structures across keys.
In Cluster mode a command spanning keys in different hash slots is rejected. Co-locate related keys with hash tags ({user:123}) when you must operate on them together (e.g. a multi-key transaction or MGET).
In production
Twitter / X
Home timelines materialized in Redis (fan-out on write)
Twitter's home timeline is one of the largest Redis deployments in production. Rather than computing each user's timeline by querying every followee at read time, Twitter fans out on write: when you tweet, the tweet id is pushed onto the Redis timeline list of each of your followers. A timeline read is then just an LRANGE of a precomputed Redis list — a few hundred microseconds — instead of a fan-in query across thousands of accounts.
The trade-offs are textbook Redis: timelines are capped (only the most recent ~800 entries are kept per user) to bound memory; the lists are a derived view rebuildable from the durable tweet store; and the celebrity fan-out problem (a tweet from an account with 100M followers would mean 100M list writes) is handled by not fanning those out — high-follower accounts are merged in at read time instead. It is the canonical illustration of "Redis as a precomputed, rebuildable serving layer in front of a durable source of truth."
Stack Overflow
The entire network cached on a tiny Redis footprint
Stack Overflow famously runs the whole Stack Exchange network on a strikingly small amount of hardware, and Redis is central to that. A single primary Redis instance (with a replica for failover) serves as the shared L2 cache across the network, sustaining on the order of a million operations per second at peak — well within Redis's single-node envelope because the operations are simple and in-memory.
The lesson their architecture writes-ups drive home is precisely Redis's design philosophy: because it is single-threaded and built on simple data structures, you can reason about its capacity directly (ops/sec, memory) and one big node goes a very long way before you need to shard. They cache rendered HTML fragments, computed values, and cross-server coordination data — keeping the SQL Server tier free for the queries that actually need it.
Good vs bad answer
Interviewer probe
“You need a leaderboard of the top 10 players by score, updated constantly. How do you build it?”
Weak answer
"Store scores in a Postgres table and run SELECT ... ORDER BY score DESC LIMIT 10 whenever someone loads the leaderboard."
Strong answer
"A Redis sorted set. ZADD leaderboard <score> <player> updates a score in O(log N), and ZREVRANGE leaderboard 0 9 returns the top 10 in O(log N + K) — it stays sorted on every write, so there's no re-sort or scan. A SQL ORDER BY score DESC LIMIT 10 works at low volume, but with constant score updates you're paying for an index maintained on a disk-based store under heavy write load; the sorted set is in-memory and built exactly for this. Postgres stays the durable source of truth for scores; the sorted set is a derived, rebuildable view. I'd also get a player's own rank for free with ZREVRANK, and if memory matters I'd periodically trim with ZREMRANGEBYRANK to keep only the top N."
Why it wins: Names the exact data structure and commands with their complexity, explains why it beats SQL ORDER BY under write load, keeps the durable store as the source of truth, and volunteers the rank query and memory-trim detail.
Interview playbook
When it comes up
- You introduce a cache, a rate limiter, a lock, a leaderboard, or "nearby" search
- A read-heavy path needs sub-millisecond latency in front of a slower store
- The interviewer asks "what specifically would you cache, and in what structure?"
- You need shared low-latency state (sessions, counters, presence) across a fleet
Order of reveal
- 11. Name the data structure, not just "Redis". I would use a Redis sorted set / hash / SET NX here — naming the structure shows I know how it behaves, not just that a cache exists.
- 22. State the durability posture. Redis is in-memory and async-replicated, so I treat it as a cache / coordination layer with the durable store as the source of truth — unless I explicitly turn on AOF or use MemoryDB.
- 33. Give the key design. Keys map to hash slots, so I design keys (and hash tags for co-location) deliberately — that is how Redis scales.
- 44. Call the hot-key risk. If one key can get disproportionately hot, I name the mitigation up front — client-side caching, key replication, or read replicas.
- 55. Size the memory. RAM is the ceiling, so I estimate the working-set memory and set TTLs / an LRU eviction policy.
Signature phrases
- “A Redis sorted set, not just "a cache".” — Naming the structure is the difference between a mid and a senior answer.
- “Redis is my cache and coordination layer; the durable DB stays the source of truth.” — Shows you respect its durability tradeoff.
- “Keys map to hash slots, so key design is how I scale.” — Demonstrates you understand cluster mechanics.
- “A lock needs a fencing token — TTL alone is not safe.” — Catches the classic distributed-lock trap.
Likely follow-ups
?“What happens to your design if the Redis primary fails?”Reveal
With a replica + Sentinel (or cluster), a replica is promoted automatically, but because replication is async the last few writes that the primary acked may be lost. For a cache that just means a brief cold patch (rewarm from the DB). For anything where those writes matter, I either use WAIT to block until a replica acks, switch to a durable variant (MemoryDB), or keep the authoritative copy in the durable store and treat Redis as derived. The key is to state the data-loss window rather than pretend failover is lossless.
?“Your cache has a hot key — one product getting 90% of traffic. How do you handle it?”Reveal
First recognise it: a hot key concentrates load on the one node owning its slot, so adding nodes does not help. Mitigations, cheapest first: (1) a short-TTL in-process cache on the app servers so most reads never reach Redis; (2) replicate the value under several keys (product:123:a..d) and randomise reads to spread across slots; (3) add read replicas for that shard. I would also make sure the value is small so each read is cheap.
?“When would you NOT use Redis here?”Reveal
When the data is the source of truth and losing seconds of writes is unacceptable, when the working set is far bigger than RAM is affordable, when I need rich queries / joins / strong multi-key transactions (Postgres), or when I need a high-throughput durable log with long retention and replay (Kafka). Redis is the versatile default for hot, ephemeral, or coordination state — not for everything.
Worked example
Setup. The interviewer asks for the real-time leaderboard of a mobile game with 50M players — show a player their global rank and the top 100, updated the instant a score changes, at tens of thousands of score updates per second.
The move. This is a textbook sorted set. One key, leaderboard:global, holds every player keyed by score. A score update is ZADD leaderboard:global <score> <player_id> — O(log N), so even at 30K updates/sec a single Redis node is comfortably inside its ~100K-ops/sec envelope. The top 100 is ZREVRANGE leaderboard:global 0 99 WITHSCORES (O(log N + 100)); a player's own rank is ZREVRANK leaderboard:global <player_id> (O(log N)). No re-sort, no scan — the structure stays ordered on every write, which is exactly what a SQL ORDER BY ... LIMIT cannot do cheaply under this write load.
Source of truth + durability. Redis is the serving tier, not the system of record — the authoritative scores live in the primary database (or are emitted as events). The sorted set is a derived, rebuildable view: if the Redis node is lost, I replay scores from the DB to rebuild it. Because losing the leaderboard for a few seconds during a failover is acceptable, async replication with a replica + Sentinel is fine; I would not pay for AOF-always here.
Memory + scale. 50M members × ~(score + id + skip-list overhead) ≈ a few GB — comfortably in RAM on one node, so I do not even need Cluster for the global board. Where I would shard is per-region or per-cohort boards (leaderboard:eu, leaderboard:friends:<id>), which also keeps each board small and cheap to query.
What breaks. The hot path is the top-100 read, which every client polls. If that becomes a hot key, I add a 1-second in-process cache on the app servers (the leaderboard only needs ~1s freshness) so 99% of reads never reach Redis, and I serve rank lookups directly. The write path stays on Redis because ZADD is the cheap part.
The result. Sub-millisecond rank and top-N reads, 30K writes/sec on a single node with a replica for HA, and a board that is always perfectly ordered — built from three commands, with the durable store behind it for recovery.
Cheat sheet
- •In-memory, single-threaded → sub-ms, ~100K–300K ops/sec/node, every command atomic.
- •Name the data structure: string, hash, list, set, sorted set, stream, geo, HLL, bitmap.
- •Cache (TTL + LRU), lock (SET NX + fencing token), leaderboard (sorted set), rate limit (INCR+EXPIRE), geo (GEOSEARCH), queue (streams), fan-out (pub/sub).
- •Durability is a dial: RDB snapshot, AOF everysec (~1s loss), AOF always (slow). Default ≈ cache.
- •Cluster = 16,384 hash slots; one command stays in one slot; use hash tags to co-locate.
- •RAM is the ceiling — size the working set, set TTLs, avoid big keys and O(N) commands.
- •Replication is async → failover can lose the last writes; keep the source of truth in a durable store.
Drills
Why can Redis implement an atomic rate-limit counter without explicit locks?Reveal
Because it is single-threaded: commands run one at a time to completion, so an INCR is inherently race-free — there is no window for two clients to read-modify-write the same counter concurrently. That is also why a multi-step limiter (read timestamps, trim, count) is wrapped in a Lua script: the script runs as one atomic unit on that same single thread.
Interviewer: "is a Redis distributed lock safe?"Reveal
Mutually exclusive and crash-safe (TTL prevents deadlock), but not correct on its own: a holder that stalls past the TTL can have the lock stolen and then both write. You make it safe with a fencing token — a monotonic number issued with the lock that the protected resource validates, rejecting stale tokens. For airtight consensus (not just best-effort mutual exclusion), use etcd/ZooKeeper instead.
Your Redis node is at 90% memory and you keep adding keys. What happens, and what do you set?Reveal
With the default noeviction policy, once memory is full Redis starts rejecting writes with an error. For a cache you want it to shed cold data instead: set maxmemory and an eviction policy like allkeys-lru (or allkeys-lfu for skewed popularity). Combined with per-key TTLs, the cache self-trims. If every key truly matters, keep noeviction but alert on memory and scale before you hit the wall.
What it is