Redis Deep Intuition
An experienced engineer's guide to Redis
1. One-Sentence Essence
Redis is a single-threaded, in-memory data-structure server: a network daemon that holds your data structures in RAM and lets many clients manipulate them one command at a time, in a strict, predictable order.
Read that again, because every surprising thing Redis does follows from it. Not “a cache.” Not “a key-value store.” Those are uses. What it is is a program that keeps hashes, sorted sets, lists, and strings in memory and exposes operations on them over a socket, executing those operations one at a time on a single thread. Caching is just the most common thing people do with that capability. Once “single-threaded in-memory data-structure server” is lodged in your head, the atomicity guarantees, the latency cliffs, the persistence tradeoffs, and the scaling story all stop being trivia and start being consequences.
2. The Problem It Solved
Rewind to 2009. Salvatore Sanfilippo (“antirez”) was building a real-time analytics product for website traffic. He needed to keep counters and lists that updated thousands of times a second. The obvious tool was MySQL. The obvious tool was also unusable: every increment was a disk-backed transaction, every “give me the latest N items” was a query the planner had to think about, and the whole thing fell over under write load that, conceptually, was trivial — just bumping numbers and pushing items onto lists.
The deeper problem was an impedance mismatch. Application code thinks in data structures — lists, sets, maps, counters. Relational databases think in tables, rows, and durable transactions. Every time you wanted to maintain “the 100 most recent events” or “the set of users currently online,” you paid the full cost of a general-purpose, disk-first, ACID database to do something a plain in-memory linked list or hash set would do in nanoseconds. People papered over this with memcached, but memcached only stored opaque blobs — you could GET and SET a string, and that was essentially it. If you wanted to add one item to a cached list, you fetched the whole list, deserialized it, appended, reserialized, and wrote it back. Race conditions everywhere, and O(n) work for an O(1) idea.
Sanfilippo’s insight was to flip the priorities. Start from RAM, not disk. Expose data structures, not blobs — so the server itself knows what a list is and can append to it atomically in one round trip. And keep the execution model dead simple: one thread, one command at a time, no locks. Disk persistence became a feature you bolt on for durability, not the foundation everything is built around. The result was a server that could do hundreds of thousands of operations per second on a single core, where adding an element to a set was SADD — one command, one network round trip, atomic by construction.
That inversion — memory-first, data-structure-native, single-threaded — is the whole ballgame. Everything Redis is good at and everything it’s bad at traces back to those three decisions.
3. The Concepts You Need
Before the mental model lands, you need the vocabulary. Redis has its own, and a lot of it concerns how data is physically represented, because that’s where the memory and performance story lives.
The data model
- Keyspace: the flat global namespace of all keys in one Redis database. There’s no nesting, no tables. Just
key → value, where the value is one of a handful of typed data structures. Keys are binary-safe strings (usually you make them human-readable likeuser:1001:session). - Database (DB): Redis has numbered logical databases (0–15 by default), selected with
SELECT. This is a vestigial feature — don’t use it for multi-tenancy. In Cluster mode only DB 0 exists. Treat “one Redis = one logical dataset” as the rule. - Value types: the structures a key can hold — string, list, set, hash, sorted set (zset), stream, plus specialty types (bitmaps, HyperLogLog, geo, and as of Redis 8, vector sets). Each type has its own command family (
SADDfor sets,ZADDfor zsets,HSETfor hashes). - TTL / expiration: a key can be given a time-to-live (
EXPIRE,SETEX), after which Redis deletes it. This is what makes Redis a cache rather than just a store.
How values are physically stored (this matters more than you’d think)
redisObject(robj): every value is wrapped in a small header struct recording its type (list, set…), its encoding (the physical layout), a reference count, and an LRU/LFU access timestamp. The encoding is the crucial part.- Encoding: the same logical type has multiple physical representations, and Redis silently picks one based on size. A small hash is stored as a listpack (a flat, contiguous byte array — no pointers, cache-friendly, O(n) but tiny so it’s fast). A large hash is stored as a hashtable (real O(1) lookups, but pointer overhead per entry). You’ll meet:
listpack(small collections, replaced the oldziplistin Redis 7+),intset(sets of only integers, a sorted packed array),hashtable(the general dict),skiplist(large sorted sets),quicklist(lists — a linked list of listpack nodes),embstr/raw/int(the three string encodings). Check any key withOBJECT ENCODING mykey. - SDS (Simple Dynamic String): Redis’s own string type, used internally for all string data and keys. It stores length and remaining capacity alongside the bytes, so getting a length is O(1) and it’s binary-safe (can hold null bytes, unlike C strings). You never touch it directly, but it’s why Redis strings are fast and can hold anything.
- Encoding conversion is one-way: once a hash grows past the threshold and converts from
listpacktohashtable, it never converts back, even if you delete elements. Remember this; it’s a real gotcha (Section 7).
Execution and concurrency
- Single-threaded command execution: one main thread executes every command, in arrival order, to completion, before starting the next. (Since Redis 6, network I/O can be offloaded to extra threads, and some slow housekeeping runs on background threads — but the actual data manipulation is still one command at a time.) This is the source of Redis’s atomicity.
- Event loop: the main thread runs an epoll/kqueue-based loop (
ae, “async events”) that watches all client sockets, reads ready commands, executes them, and writes responses. Thousands of idle connections cost almost nothing because the loop only wakes for sockets with actual data. - Atomicity: because one command runs at a time with no preemption, every single command is atomic for free — no locks, no mutexes, no race conditions on the data.
INCRcan never lose an increment. This is a consequence of single-threading, not an added feature. - Pipelining: a client can send many commands without waiting for each reply, then read all replies at once. This doesn’t change execution semantics — it just amortizes network round-trip time. Huge throughput win.
- RESP / RESP3: the Redis Serialization Protocol — the simple, line-based wire format clients and server speak. It’s why writing a Redis client is a weekend project. RESP3 (Redis 6+) is the upgraded version; its key addition is push messages — the server can send a client unsolicited data on the same connection alongside command replies. That one capability is what makes server-assisted client-side caching practical (see judgment call 11). Clients opt in with the
HELLO 3handshake.
Durability and topology
- RDB: point-in-time binary snapshot of the whole dataset, written by forking a child process. Compact, fast to load, but you lose everything since the last snapshot on a crash.
- AOF (Append-Only File): a log of every write command, replayed on restart. More durable, larger, slower to load. Can be combined with RDB (hybrid).
fork()+ copy-on-write: how Redis snapshots without blocking. The OS clones the process; parent and child share memory pages until one is modified, at which point that page is copied. This is why snapshotting a large, write-heavy instance can briefly balloon memory and CPU.- Replication: a replica connects to a primary and receives a continuous stream of writes, kept asynchronously in sync. Replication is asynchronous — the primary does not wait for replicas to acknowledge. This is the root of Redis’s weak consistency.
- Sentinel: a separate set of processes that monitor a primary-replica setup and perform automatic failover (promote a replica when the primary dies). For non-sharded HA.
- Cluster: Redis’s built-in sharding. The keyspace is split into 16,384 hash slots; each primary owns a range of slots; nodes gossip about who owns what and who’s alive. This is how you scale past one machine’s RAM or one core’s throughput.
Everything below leans on these. If listpack, fork-COW, asynchronous replication, and single-threaded execution feel solid, the rest will click.
4. The Distilled Introduction
This is the part a 10-hour course spreads across a dozen videos. Here it is compressed to what matters.
Setup
Install and run:
# Debian/Ubuntu: get a recent build, not the ancient distro package
sudo apt install redis-server
redis-server # foreground, default config, port 6379
redis-cli # interactive client
redis-cli -h host -p 6379 # connect to a remote instance
In production you run redis-server /etc/redis/redis.conf with a tuned config. The single most important early decision is maxmemory and maxmemory-policy — set them, always (see Section 7; the default of unlimited memory + noeviction is a footgun).
Sanity check inside redis-cli:
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> SET hello world
OK
127.0.0.1:6379> GET hello
"world"
Strings — the workhorse
A Redis string is any byte sequence up to 512MB: text, JSON, a serialized protobuf, a counter, a JPEG. The killer feature is atomic numeric ops:
SET page:views 0
INCR page:views # -> 1, atomic, no read-modify-write race
INCRBY page:views 10 # -> 11
SET session:abc "{...}" EX 3600 # set with a 1-hour TTL in one command
SETNX lock:job "held" # set only if absent — a naive lock primitive
GET page:views
INCR is the canonical example of why a data-structure server beats a blob cache: the increment happens inside Redis, atomically, in one round trip. With memcached you’d fetch, parse, add, write back — and lose increments under concurrency.
Hashes — objects without the serialization tax
A hash is a map of field → value under one key. Use it for objects so you can read/write individual fields without touching the whole thing:
HSET user:1001 name "Ada" email "ada@x.com" logins 0
HINCRBY user:1001 logins 1 # bump one field atomically
HGET user:1001 email # read one field
HGETALL user:1001 # read everything (careful on huge hashes)
Small hashes are wildly memory-efficient because they’re stored as a single listpack — one object header for the whole thing instead of one per field. This is the basis of a classic optimization: storing millions of small objects as fields inside a few thousand hashes instead of millions of top-level keys.
Lists — queues and stacks
A list is an ordered sequence, fast at both ends, implemented as a quicklist (linked list of listpack nodes):
LPUSH queue:jobs "job1" # push left
RPUSH queue:jobs "job2" # push right
RPOP queue:jobs # pop right -> FIFO queue when paired with LPUSH
LRANGE queue:jobs 0 -1 # read a range; 0 -1 = everything
LTRIM feed:user:1 0 99 # keep only the newest 100 — capped list
BRPOP queue:jobs 5 # BLOCKING pop, wait up to 5s — real work-queue primitive
LPUSH + BRPOP is a genuine producer/consumer queue. LPUSH + LTRIM is a capped “latest N” feed. Note both ends are O(1) but indexing into the middle is O(n) — lists are not arrays.
Sets — membership and uniqueness
Unordered, unique members. Two encodings: intset (all-integer, packed) and hashtable/listpack:
SADD online:users 1001 1002 1003
SISMEMBER online:users 1001 # O(1) membership test
SCARD online:users # count
SINTER following:1 following:2 # users both follow — set intersection in the server
Set operations (SINTER, SUNION, SDIFF) run inside Redis on the whole set — powerful, but watch the cost on large sets (Section 7).
Sorted sets — the crown jewel
Every member has a floating-point score; members are kept ordered by score. Backed by a skiplist + hashtable combo (the hashtable gives O(1) score lookup, the skiplist gives O(log n) ordered access). This is the structure people underuse and shouldn’t:
ZADD leaderboard 100 "ada" 95 "lin" 99 "kai"
ZRANGE leaderboard 0 9 REV WITHSCORES # top 10 by score
ZRANK leaderboard "ada" # what rank is Ada?
ZINCRBY leaderboard 5 "lin" # bump a score atomically
ZRANGEBYSCORE events 1700000000 1700003600 # range query by score
Leaderboards, priority queues, rate limiters, time-ordered indexes (use a timestamp as the score), “things due before time T” — all sorted sets.
Expiration and the cache pattern
SET cache:product:42 "{...}" EX 300 # expire in 300s
TTL cache:product:42 # seconds left, -1 = no TTL, -2 = gone
PERSIST cache:product:42 # remove the TTL
EXPIRE session:abc 1800 # (re)set TTL on existing key
The cache-aside pattern in one breath: GET the key; on miss, load from the database, SET it with a TTL, return it. TTL is what bounds memory and keeps data fresh.
Atomic multi-step operations: MULTI, Lua, and functions
When you need several commands to run together with nothing in between, you have three tools:
MULTI # start a transaction (queue commands)
INCR balance:a
DECRBY balance:b 50
EXEC # run the whole queue atomically, no other client interleaves
MULTI/EXEC queues commands and runs them as one atomic unit — but note it’s not a rollback-on-error transaction in the SQL sense; it’s “these run together without interruption.” For conditional logic, use WATCH (optimistic locking — abort if a watched key changed) or, far more commonly in practice, a Lua script via EVAL, which runs server-side, atomically, with full logic:
-- Atomic "decrement stock only if available" — runs as one indivisible unit
-- KEYS[1] = stock key, ARGV[1] = amount
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
return redis.call('DECRBY', KEYS[1], ARGV[1])
else
return -1
end
Lua scripts are the standard way to do read-decide-write atomically. Because Redis is single-threaded, the entire script runs with no other command interleaving — which is exactly why a slow Lua script is dangerous (it blocks everything). Redis 7 added Functions (FUNCTION LOAD), a more managed, persistent evolution of the same idea.
Pub/Sub and Streams — messaging
SUBSCRIBE news # client A listens
PUBLISH news "hello" # client B broadcasts; A receives it
Pub/Sub is fire-and-forget: if no one is subscribed, the message vanishes. No persistence, no replay, no acknowledgment. It’s the right tool only when “missed it = didn’t matter” — live dashboards, cache-invalidation hints, presence pings.
When you need delivery guarantees, replay, and multiple cooperating consumers, use Streams — an append-only log structure (a radix tree of listpacks, see Section 3) introduced in Redis 5. Think a lightweight Kafka living inside Redis:
XADD events * type "signup" user "1001" # append an entry; * = auto-generate a time-ordered ID
XLEN events # how many entries
XRANGE events - + # read all entries (— to +)
XREAD COUNT 10 STREAMS events 0 # read from the beginning, like tailing a log
# Consumer groups: multiple workers split the work, each entry delivered once per group
XGROUP CREATE events workers $ # create group "workers", start at newest ($)
XREADGROUP GROUP workers worker-1 COUNT 1 STREAMS events > # claim the next unprocessed entry
XACK events workers 1700000000000-0 # acknowledge — removes it from the pending list
XAUTOCLAIM events workers worker-2 30000 0 # steal entries a dead worker claimed but never ACKed
The mental model for Streams: every entry has a time-ordered ID; a consumer group tracks a shared cursor and a pending-entries list (PEL) of entries that were delivered but not yet XACK’d. If a worker crashes mid-job, its claimed-but-unacked entries sit in the PEL until another worker steals them with XAUTOCLAIM. That’s the durability Pub/Sub lacks — at-least-once delivery with crash recovery. The trade you make versus Kafka: a stream lives in one Redis key on one shard (no native partitioning across the cluster), and like everything else it’s bounded by RAM, so you cap growth with XADD ... MAXLEN ~ 1000000 (the ~ makes trimming approximate and cheap).
The specialty types — small structures, big leverage
Beyond the five core types, Redis ships a few purpose-built structures that look like party tricks and turn out to be load-bearing in real systems:
# Bitmaps — a string treated as a bit array. One bit per user = 125KB for a million users.
SETBIT active:2026-06-25 1001 1 # mark user 1001 active today
BITCOUNT active:2026-06-25 # how many active? (population count)
BITOP AND active:both active:mon active:tue # users active both days
# HyperLogLog — approximate unique-count in a fixed 12KB, regardless of cardinality
PFADD visitors:today 1001 1002 1003 # add elements
PFCOUNT visitors:today # ~unique count, ~0.81% standard error
PFMERGE visitors:week visitors:mon visitors:tue # union of HLLs, still 12KB
# Geospatial — sorted set under the hood, scored by geohash
GEOADD cities 13.361 38.115 "Palermo"
GEOSEARCH cities FROMMEMBER "Palermo" BYRADIUS 200 km ASC # nearby members
These exist because of Core Idea 2 (Section 5): memory is the scarce resource, so Redis offers structures that answer expensive questions in fixed, tiny space. HyperLogLog is the showpiece — counting unique visitors across a billion events would cost gigabytes in a set, but an HLL holds the estimate in 12KB at ~0.81% error. When someone asks “how many unique X today/this week/this month” and exactness doesn’t matter, this is the answer. Bitmaps do the same trick for dense boolean-per-id data (daily-active flags, feature cohorts). They’re not data you’d reach for daily, but knowing they exist is the difference between a clever O(1)-space solution and an accidental memory blowout.
Vector sets — similarity search as a native data structure (Redis 8)
The newest type, and the reason Redis shows up in AI conversations in 2026. A vector set stores high-dimensional embeddings and answers “what’s most similar to this?” using an HNSW graph (hierarchical navigable small world — approximate nearest-neighbor search in O(log N)). Created by antirez and shipped with Redis 8:
# Add an embedding (FP32 blob or explicit values) with an element name
VADD movies VALUES 3 0.1 0.2 0.3 "Inception"
VADD movies VALUES 3 0.15 0.18 0.31 "Interstellar"
VSIM movies VALUES 3 0.1 0.2 0.3 COUNT 5 WITHSCORES # 5 nearest neighbors + similarity scores
VSIM movies ELE "Inception" COUNT 5 # neighbors of an existing element
# Attach JSON attributes and filter the similarity search by them (hybrid search)
VSETATTR movies "Inception" '{"year": 2010, "genre": "scifi"}'
VSIM movies ELE "Inception" FILTER '.year >= 2000 and .genre == "scifi"'
The design choice that makes it Redis rather than a bolt-on index: it’s a data structure, not an index over other keys. You VADD items the way you’d SADD them, and the graph is serialized to disk as a graph — so after a restart you reload in seconds instead of paying the minutes-long insertion cost to rebuild the HNSW. Two pragmatic details worth carrying: vectors are int8-quantized by default (Q8), giving ~4× memory savings with near-negligible recall loss — antirez reports 3 million 300-dimension vectors fit in ~3GB (~1KB each), serving ~50K VSIM/sec on a laptop. And uniquely in Redis, similarity searches are threaded — antirez made an explicit exception to the single-thread rule (Core Idea 1) because vector math is genuinely slow relative to everything else Redis does. Inserts (VADD) stay single-threaded and run at a few thousand/sec, much slower than searches. If you’re building RAG or semantic search and already run Redis, this can replace a separate vector database for small-to-medium corpora; for billions of vectors with heavy write churn, a dedicated vector store still wins.
Inspecting what’s going on
INFO # giant status dump: memory, clients, replication, persistence
INFO memory # just the memory section
DBSIZE # number of keys
OBJECT ENCODING mykey # which physical encoding a value uses
MEMORY USAGE mykey # bytes a key consumes
SCAN 0 MATCH user:* # cursor-based iteration — NEVER use KEYS in production
SLOWLOG GET 10 # the 10 slowest recent commands
That SCAN-not-KEYS line is one of the highest-value things in this whole document. We’ll see why in Section 7.
5. The Mental Model
Four core ideas. Internalize these and you can predict Redis behavior you’ve never seen.
Core Idea 1: One thread, one command at a time, run to completion.
There is a single thread executing your commands, in the order they arrive, and it does not move to the next command until the current one finishes. There is no preemption, no time-slicing of command execution.
What this predicts:
- Every command is atomic, for free. No locks needed.
INCR,SADD,LPUSHcan never interleave or corrupt. This is why Redis is the go-to for distributed counters and locks. - A slow command freezes the entire server.
KEYS *on 10 million keys,SMEMBERSon a huge set, aLuascript with a loop,FLUSHALLon a giant DB — while any of these runs, every other client waits. There is no “this query is slow but others proceed.” Latency is shared. This single fact explains most Redis production incidents. - More cores don’t help command throughput. One instance saturates one core for command processing. To use 32 cores you run a Cluster of shards. Vertical scaling of CPU is mostly pointless for Redis.
- You can reason about correctness without thinking about concurrency. The execution is effectively serial. A Lua script is atomic because nothing can run during it.
Core Idea 2: It all lives in RAM; disk is an afterthought for durability.
The authoritative copy of your data is in memory. Persistence (RDB/AOF) exists so you can recover after a restart, not so Redis can serve from disk. Redis never reads from disk to answer a query.
What this predicts:
- Your dataset must fit in RAM (plus headroom for replication buffers, COW during snapshots, and fragmentation — budget ~1.5–2× your data size). When it doesn’t fit, you either evict, shard, or fall over.
- Restart means reload-from-disk, and load time scales with data size. A 50GB AOF can take minutes to replay. RDB loads faster than AOF.
- Durability is a spectrum you choose, and the default is weak. Out of the box you can lose seconds-to-minutes of writes on a crash. If you assumed “it’s a database, it’s durable,” you assumed wrong (Section 11).
- Memory is the scarce resource, so encodings, key overhead, and TTLs matter enormously. A million tiny keys can cost far more in per-key overhead than the data itself.
Core Idea 3: Values are typed data structures with size-dependent physical encodings.
A key doesn’t hold “a value” — it holds a typed structure (hash, zset…) that Redis stores in one of several physical layouts depending on its size. Small collections get compact, contiguous, pointer-free encodings (listpack, intset); large ones get real hash tables and skip lists.
What this predicts:
- Many small structures are dramatically more memory-efficient than the same data spread across many keys, because the compact encoding shares one object header across all entries. This is the basis of the “shard your keyspace into hashes” optimization.
- Performance characteristics change with size. A small
listpackhash has O(n) field access — but n is tiny and it’s all in one cache line, so it’s faster than the O(1) hashtable for small n. Cross the threshold and you flip to O(1) hashtable with pointer overhead. - Encoding conversion is a real, one-way event. Crossing
hash-max-listpack-entriespermanently converts the structure tohashtable, and it won’t convert back when it shrinks. Predictable memory behavior requires knowing your thresholds. - Choosing the right type is a design decision with memory and CPU consequences, not just an API choice. Using a sorted set where a set would do, or top-level keys where a hash would do, can cost you multiples in RAM.
Core Idea 4: Replication is asynchronous, so Redis trades consistency for speed and availability.
A primary streams writes to replicas without waiting for acknowledgment. Failover promotes a replica that may be slightly behind.
What this predicts:
- Acknowledged writes can be lost. The primary says “OK,” then crashes before the write reaches any replica; the promoted replica never saw it. This is by design. Redis is an AP-leaning system, not a CP one.
- Replicas serve stale reads. A read from a replica may be milliseconds-to-seconds behind the primary. Fine for caching, dangerous for read-after-write correctness.
- Failover is best-effort, not graceful. Sentinel and Cluster resolve “who is primary” deterministically eventually, but a network partition can briefly produce a split where an isolated old primary accepts writes that later get discarded.
- If you need stronger guarantees, you bolt them on (
WAIT,min-replicas-to-write) and pay in latency and availability — you cannot make Redis linearizable just by configuration. Don’t use vanilla Redis as your system of record for money without understanding exactly this.
6. The Architecture in Plain English
Let’s trace what actually happens, end to end.
A command arrives. A client opens a TCP connection and speaks RESP. The main thread’s event loop (ae, wrapping epoll on Linux) is parked in a syscall waiting for any of its thousands of sockets to become readable. Your client’s bytes arrive; the loop wakes, sees that socket is ready, and reads the command. Since Redis 6, the reading and parsing of the bytes off the socket can be handed to a small pool of I/O threads to parallelize network work — but the moment the command is parsed, it comes back to the one main thread for execution.
The command executes. The main thread looks up the key in the global dictionary (a hash table mapping key → redisObject). It dispatches to the command implementation (SADD, ZRANGE, whatever), which manipulates the in-memory structure directly. No locks are taken because no other thread can touch this data. The operation completes fully. The reply is written to the client’s output buffer. Then — and only then — the loop moves to the next ready event. This is the serial heart of Redis: lookup, execute, reply, next.
Where state lives. Everything is in that one process’s heap: the global keyspace dict, every value structure, the replication backlog buffer, client output buffers, and Lua/Function state. There is no shared memory, no separate storage engine process, no buffer pool abstraction over disk. The data is the process’s memory.
Expiration happens lazily and actively. A key with a TTL isn’t deleted the instant it expires. Redis uses two mechanisms: lazy (when you access an expired key, it’s deleted then and a miss returned) and active (a background cycle samples random keys with TTLs ~10x/second and evicts the expired ones). This is why expired keys can briefly still occupy memory, and why a flood of simultaneous expirations can cause a CPU blip.
Persistence forks a child. When it’s time to snapshot (RDB) or rewrite the AOF, the main thread calls fork(). The OS creates a child process that shares all memory pages copy-on-write. The child walks the (frozen, consistent) memory image and writes it to disk while the parent keeps serving. As the parent modifies data, the OS copies the affected pages so the child still sees the old version. The cost: on a write-heavy instance, many pages get copied, so memory usage can spike toward 2× during the snapshot, and the fork() itself pauses the main thread for a moment proportional to the process’s page-table size (bigger on huge instances). This is why fork-based persistence on a 100GB instance is something you tune carefully or avoid.
Replication streams the writes. A replica handshakes with the primary, gets an initial RDB snapshot (a full sync), loads it, and then receives a continuous feed of every write command via the replication backlog. If the link drops briefly, it can do a partial resync from the backlog buffer rather than a full re-transfer. The primary never waits for the replica — it has already replied “OK” to the client by the time the write streams out. That asynchrony is Core Idea 4 made concrete.
Cluster shards by hash slot. In Cluster mode, the key’s “hash tag” (or whole key) is run through CRC16 and taken mod 16,384 to pick a hash slot; each primary owns a contiguous-ish set of slots. Nodes run a gossip protocol over a separate cluster-bus port, constantly exchanging “who’s alive, who owns which slots.” When a client sends a command to the wrong node, that node replies with a MOVED or ASK redirect pointing to the right shard, and a cluster-aware client caches the slot map so it routes directly thereafter. Multi-key commands only work if all keys hash to the same slot — which is why you use hash tags like {user:1001}:profile and {user:1001}:sessions to force related keys onto one shard.
Failover, when a primary dies. In a Sentinel setup, Sentinels ping nodes; when enough of them agree a primary is down (quorum → “objectively down”), they elect a leader Sentinel that promotes a replica. In Cluster mode, the replicas and primaries gossip-detect the failure and the replicas of the dead primary run an election among themselves, with the majority side of the cluster authorizing the promotion. Both are brute-force, not graceful: in-flight unreplicated writes are simply lost.
The performance envelope — actual numbers to carry in your head
Vague magnitudes (“it’s fast”) aren’t useful for capacity planning. Here are figures grounded in Redis’s own benchmarks and antirez’s measurements. Treat them as orders of magnitude on commodity hardware, not guarantees — your network, payload size, and command mix move them.
Latency. A single command’s processing time inside Redis is sub-microsecond to a few microseconds — the data is in RAM and the operation is usually O(1) or O(log n). What you actually measure from a client is dominated by the network round trip, not Redis: on a healthy LAN, end-to-end median latency is well under 1 millisecond (often ~100–250µs). The implication: if you see millisecond-plus latencies, suspect the network, a slow command blocking the thread, or a fork pause — almost never Redis “thinking.”
Throughput, one instance, no pipelining. A single instance handles roughly 100,000–200,000 simple ops/sec (GET/SET/INCR) on modern hardware with many concurrent connections. This is the number most people should plan around, because it’s what you get without special effort. Unix domain sockets (local client) buy ~50% over TCP loopback.
Throughput with pipelining. Batch commands per round trip (-P 16) and the same instance jumps to 500,000–1,000,000+ ops/sec, because you’ve amortized away the network round trip that was the real bottleneck. This is why pipelining (judgment call 8) is the single biggest throughput lever you have — it’s frequently a 5–10× win for nothing.
Scaling out. One instance saturates one core (Core Idea 1), so to go further you shard. Redis Ltd.’s published Enterprise benchmark hit ~800K ops/sec per shard and 200M ops/sec across 40 nodes with sub-millisecond latency — the point isn’t the headline number, it’s that throughput scales roughly linearly with shards because shards share nothing.
“How big is a big key?” A useful rule of thumb: individual collections (a single hash, set, list, zset) should stay in the low thousands of elements for hot-path operations, and a single value should stay well under ~1MB. A collection of tens of thousands of elements is a yellow flag; hundreds of thousands to millions is a red one, because any O(n) operation on it (SMEMBERS, HGETALL, serialization for replication, DEL) blocks the single thread for the whole traversal. redis-benchmark’s own guidance and the latency docs flag values over ~10KB as the point where memory bandwidth starts to matter and over ~1MB as actively dangerous. When a structure must hold millions of entries, shard it across many keys.
Memory overhead per key. Each top-level key costs more than its data: a redisObject header, the SDS for the key string, and a dictEntry in the global hash table — call it ~50–100 bytes of overhead per key before the value. A million tiny top-level keys can cost more in bookkeeping than in payload. This is the arithmetic behind the “pack small objects into hashes” optimization (Core Idea 3): one listpack-encoded hash with 100 fields shares a single object header instead of paying per-key overhead 100 times.
Fork pause. The fork() for snapshotting pauses the main thread for a time proportional to the page-table size, roughly ~12ms per GB of instance memory on a typical Linux/AMD64 box (a 24GB instance → ~48MB of page tables to copy). On a 100GB write-heavy instance this is a real, recurring latency spike — which is why you either offload persistence to a replica or disable THP (transparent huge pages) and tune vm.overcommit_memory to keep it tolerable.
7. The Things That Bite You
Each of these maps directly to a mental-model idea. That’s not a coincidence — gotchas are mental models you haven’t internalized yet.
1. KEYS * will take down production. You’d expect a “list my keys” command to be cheap. It’s O(n) over the entire keyspace, and because of Core Idea 1, it blocks every other client for the whole scan. On millions of keys that’s seconds of total freeze. Always use SCAN (cursor-based, incremental, O(1) per call) instead. Same logic forbids SMEMBERS on huge sets, HGETALL on huge hashes, and LRANGE 0 -1 on huge lists in hot paths.
2. Big keys are latency bombs. A single 5-million-element set or a 50MB value is fine to read by reference but catastrophic to operate on: deleting it, serializing it for replication, or iterating it blocks the one thread. Worse, DEL on a giant key is synchronous and O(n). Use UNLINK instead (frees memory on a background thread). Hunt big keys with redis-cli --bigkeys and design them out — split a giant set into shards.
3. Memory isn’t returned to the OS when you delete keys. You delete 2GB of data and used_memory drops, but the OS still shows ~5GB RSS. This is malloc/jemalloc behavior (and Core Idea 2 making memory the main resource): freed memory is held for reuse, not handed back. You’ll see a high mem_fragmentation_ratio. Fix with activedefrag yes or a restart. Don’t panic at the ratio right after a big deletion — it’s expected.
4. The default eviction policy is noeviction, and no maxmemory is set. Out of the box, Redis will happily grow until it eats all RAM and the Linux OOM killer murders the process — or, with a limit but noeviction, it starts rejecting writes with OOM command not allowed. For a cache you almost always want maxmemory set to ~75% of RAM and allkeys-lru (or allkeys-lfu). Setting these on day one prevents a whole category of 3am pages.
5. Mixing cache and durable data under one eviction policy. If sessions and cache share an instance with allkeys-lru, an eviction storm logs your users out. If you protect them with noeviction, cache writes start failing. These are different workloads with different needs — separate them into different instances. (Core Idea 2: memory is contested; who wins eviction matters.)
6. Keys without TTLs accumulate forever. A steadily climbing key count that never plateaus almost always means someone is writing cache keys without EX. They never expire, memory creeps up, and one day you OOM. Audit with --scan --pattern and make TTLs a code-review requirement for cache writes.
7. Encoding conversion is permanent. Push a hash past hash-max-listpack-entries (default 128) and it converts from compact listpack to hashtable — and never converts back when it shrinks (Core Idea 3). A workload that briefly spikes a structure large can leave it permanently in the memory-hungry encoding. If memory efficiency matters, either keep structures below thresholds or accept the conversion deliberately.
8. Pub/Sub silently drops messages. Publishers don’t know if anyone’s listening; subscribers that disconnect miss everything sent while away. People build “reliable” notification systems on Pub/Sub and are baffled by lost messages. If you need delivery guarantees, replay, or consumer groups, use Streams, not Pub/Sub.
9. A slow Lua script (or MONITOR, or DEBUG SLEEP) freezes everything. Lua runs atomically because it blocks the single thread (Core Idea 1). A script that loops over a big collection holds the whole server hostage. Keep scripts short and bounded. Likewise, leaving MONITOR running in production (it streams every command to your client) adds real overhead.
10. FLUSHALL during an incident causes a thundering herd. When Redis is full and you panic-flush, every app instance simultaneously misses the cache and hammers the backing database, often taking that down too. Selectively delete offending keys or let eviction work. (And FLUSHALL on a big DB is itself a blocking O(n) operation.)
11. Replicas can serve stale reads, and acknowledged writes can vanish. Reading from a replica after writing to the primary may not reflect your write (async replication, Core Idea 4). And a primary crash can lose the last writes it acked but hadn’t yet streamed. If your code assumes read-after-write consistency or zero write loss, it’s wrong by default.
8. The Judgment Calls
The decisions that separate someone who’s read the docs from someone who’s run Redis in anger.
1. RDB vs AOF vs both. RDB gives compact snapshots and fast restarts but loses everything since the last snapshot. AOF (with appendfsync everysec) loses at most ~1 second of writes but produces bigger files and slower restarts. What experienced engineers do: for a pure, rebuildable cache, often no persistence at all (why pay fork costs for data you can regenerate?). For anything you’d miss, enable both with aof-use-rdb-preamble yes — the AOF starts with an RDB snapshot then appends commands, giving fast restarts and second-level durability. The signal: ask “if this instance vanishes, what breaks?” If “nothing, it refills” → no persistence. If “we lose real state” → hybrid.
2. appendfsync everysec vs always. always fsyncs every write — near-zero loss, but it throttles throughput hard and couples your latency to disk latency. everysec loses up to a second but keeps Redis fast. Almost everyone should use everysec. Reach for always only when a sub-second window of loss is genuinely unacceptable and you’ve accepted you’re now disk-bound — at which point question whether Redis is the right system of record at all.
3. Cluster vs Sentinel vs single instance. Single instance: simplest, fine until you outgrow one core or one machine’s RAM, but a SPOF. Sentinel: adds automatic failover to a primary-replica setup, no sharding — choose it when you need HA but your data fits one machine and you want multi-key commands and all 16 DBs. Cluster: shards across nodes — choose it when you’ve outgrown one machine’s RAM/throughput, accepting the costs (only DB 0, multi-key ops only within a slot, more operational complexity). The signal: don’t reach for Cluster for HA alone — Sentinel does that more simply. Reach for Cluster only when one machine genuinely isn’t enough.
4. One big instance vs many sharded shards. Because of single-threading, a single instance caps at one core’s command throughput (~tens of thousands to low hundreds of thousands ops/sec depending on command). Before Cluster, the cheaper move is often multiple independent Redis instances on the same big box, each pinned to a core, partitioned by function (one for sessions, one for cache, one for queues). This sidesteps cross-slot limitations and isolates blast radius.
5. Which data type for the job. Counter → string + INCR. Object with independently-updated fields → hash. “Latest N” or work queue → list. Membership/uniqueness/set algebra → set. Ranking, priority queue, time-ordered index, rate limiting → sorted set. Durable event log with consumers → stream. The taste: people overuse top-level keys where a hash would save memory, and underuse sorted sets where they’d replace ugly application-side sorting. When you catch yourself sorting in app code, ask if a zset score does it.
6. Eviction policy. allkeys-lru/allkeys-lfu for pure caches (evict anything). volatile-lru/volatile-ttl when some keys must never be evicted (only keys with a TTL are eligible) — but this requires discipline that durable keys never get a TTL. noeviction only when Redis is a store and you’d rather reject writes than lose data. LFU beats LRU when you have a stable hot set with occasional scans that would pollute LRU’s recency signal.
7. TTL strategy and the stampede. Setting the same TTL on many keys created together means they all expire together, causing a synchronized cache-miss stampede on your database. Add jitter — randomize TTLs by ±10%. And for hot keys, consider refreshing slightly before expiry rather than letting them lapse.
8. Pipelining and round trips. A loop doing 10,000 SETs with a round trip each is dominated by network latency, not Redis. Pipeline them (or use MSET, or a Lua script) to send in batches and collapse 10,000 round trips into a handful. This is frequently a 10–50× throughput win and costs nothing semantically.
9. Distributed locks: how much rigor. SET key val NX PX 30000 (set-if-absent with expiry) is the simple, correct-enough lock for most cases. The fancier Redlock algorithm across independent instances is more robust to single-node failure but is genuinely contested in the literature (Kleppmann vs antirez) and over-engineered for most needs. The signal: if a momentarily-double-held lock would corrupt money or violate safety, Redis locks may be the wrong tool — use a system with real consensus. Otherwise SET NX PX plus a fencing token is fine.
10. Redis vs Valkey vs a managed service (the 2024–2026 question). This is now a real decision, not a footnote. After the 2024 license change, the open-source-purist, cloud-vendor-backed choice is Valkey (BSD, Linux Foundation); Redis 8 re-added the OSI-approved AGPLv3 but the trust damage drove AWS, Google, and Oracle to standardize on Valkey in their managed offerings. The signal: if you’re on a managed service, you’re likely already being steered to Valkey and it’s a drop-in replacement. If you self-host and care about permissive licensing or want to avoid AGPL copyleft entanglement, Valkey. If you want Redis Ltd.’s integrated modules (Query Engine, JSON, vector sets) in one AGPL package, Redis 8. (Full story in Section 11.)
11. Server round trip vs client-side caching for hot reads. For a key read thousands of times a second that rarely changes (config, feature flags, a hot product record), even a sub-millisecond Redis round trip is wasted work. Server-assisted client-side caching (Redis 6+, via CLIENT TRACKING over RESP3) lets the app cache the value in local process memory and have Redis push an invalidation message when the key changes — so the local copy stays correct without a TTL guess. The server keeps an invalidation table of which clients read which keys; on a write, it notifies exactly those clients. Two modes, and choosing wrong is the trap: default mode tracks each key a client reads (precise, but costs server memory for the tracking table, capped by tracking-table-max-keys); broadcast mode (BCAST PREFIX user:) tracks prefixes with no per-client table, but floods every subscribed client with invalidations for the whole prefix. The signal: reach for client-side caching only for genuinely read-heavy, change-rarely keys — if the data churns, you generate a storm of invalidation traffic that costs more than the round trips you saved (this is the same “don’t cache things that change constantly” wisdom that governs any cache). And handle the failure mode: if the invalidation connection drops, you may have stale data, so flush the local cache on disconnect and ping the invalidation channel periodically. For most apps a plain TTL is simpler and good enough; client-side caching is the tool when you’ve measured that Redis round trips on a few hot keys are your bottleneck.
9. The Commands That Actually Matter
Grouped by what you’re trying to do. The 20% you’ll use 80% of the time, with the why.
Strings / counters
SET k v EX 300 NX— set with TTL, only if absent. The Swiss-army command; covers caching, locks, and one-shot flags in one call.GET/MGET k1 k2 k3—MGETfetches many in one round trip; prefer it over a loop ofGETs.INCR/INCRBY/INCRBYFLOAT— atomic counters, the reason data-structure servers beat blob caches.
Hashes
HSET/HGET/HMGET— read/write individual fields without touching the whole object.HINCRBY— atomic per-field counter.HGETALL— fine on small hashes, dangerous on huge ones (O(n) + blocks the thread).
Lists
LPUSH/RPUSH/LPOP/RPOP— O(1) ends; build queues and stacks.BRPOP/BLPOP— blocking pops, the basis of real work queues without polling.LRANGE/LTRIM— read ranges;LTRIMcaps a list to “latest N.”
Sets
SADD/SREM/SISMEMBER/SCARD— membership and counts.SINTER/SUNION/SDIFF— set algebra in the server (watch size).SRANDMEMBER/SPOP— random sampling / atomic random removal.
Sorted sets
ZADD/ZINCRBY— add/update scored members atomically.ZRANGE ... REV WITHSCORES/ZRANGEBYSCORE— leaderboards and range-by-score queries.ZRANK/ZSCORE/ZPOPMIN— rank lookup, score lookup, priority-queue pop.
Keys / lifecycle
EXPIRE/TTL/PERSIST— manage expiration.DELvsUNLINK—UNLINKfrees big keys on a background thread; prefer it.SCAN/HSCAN/SSCAN/ZSCAN— always overKEYS/SMEMBERS/etc. in production.TYPE/OBJECT ENCODING/MEMORY USAGE— introspect type, physical layout, and cost.
Atomicity
MULTI/EXEC/WATCH— queued transactions with optimistic locking.EVAL/EVALSHA/FUNCTION— server-side Lua / Functions for atomic read-decide-write logic.
Streams (durable messaging)
XADD ... MAXLEN ~ N— append an entry; the~ Ncaps growth cheaply.XREADGROUP/XACK— consumer-group read and acknowledge (at-least-once delivery).XAUTOCLAIM— recover entries a crashed consumer claimed but never acked.
Vector sets (similarity search, Redis 8)
VADD/VSIM— add an embedding / find nearest neighbors.VSETATTR+VSIM ... FILTER— attach JSON attributes and do hybrid filtered search.VINFO/VCARD/VDIM— inspect quantization, size, dimensions.
Specialty counters
PFADD/PFCOUNT/PFMERGE— HyperLogLog approximate unique counts in fixed 12KB.SETBIT/BITCOUNT/BITOP— bitmap operations for dense boolean-per-id data.
Operations / debugging
INFO [section]— the dashboard: memory, clients, replication, persistence, stats.SLOWLOG GET— the slowest recent commands; first stop for latency complaints.MEMORY DOCTOR/--bigkeys/LATENCY DOCTOR— memory, big-key, and latency diagnosis.CONFIG GET/CONFIG SET/CONFIG REWRITE— inspect and change config at runtime, then persist it.CLIENT LIST/CLIENT KILL/CLIENT TRACKING— see/cut connections; enable client-side caching.
10. How It Breaks
The failure modes you’ll actually meet, and how to think about each.
Out of memory. Symptoms: OOM command not allowed errors (with a maxmemory limit + noeviction), or the process getting killed by the Linux OOM killer (no limit set). Root cause (Core Idea 2): dataset grew past RAM — usually keys without TTLs, an unexpected big key, or a missing maxmemory. Diagnose: INFO memory for used_memory vs maxmemory and mem_fragmentation_ratio; --bigkeys; MEMORY DOCTOR; scan key patterns for runaway growth. Fix: set maxmemory and an eviction policy, add TTLs, UNLINK the offenders, or scale out. Don’t FLUSHALL (thundering herd).
Latency spikes / everything slow at once. Symptoms: all clients see high latency simultaneously, not just some. Root cause (Core Idea 1): one slow command is blocking the single thread — KEYS, a big-key operation, an unbounded Lua script, a synchronous DEL of a huge key, or fork pauses during snapshotting. Diagnose: SLOWLOG GET is the first move; check INFO for latest_fork_usec and whether a save was in progress; look for big keys. Fix: replace the offending command pattern (SCAN, UNLINK, bounded scripts), tune or disable fork-based persistence on huge instances, offload reads to replicas.
Fork failures during snapshot. Symptoms: BGSAVE/AOF-rewrite fails; logs mention fork errors; latency spike or OOM during save. Root cause: fork() + copy-on-write needs enough memory headroom for copied pages on a write-heavy instance; if the box is near full RAM, the fork can fail or trigger OOM. Diagnose: check free system RAM vs Redis RSS, latest_fork_usec, rdb_last_bgsave_status. Fix: leave RAM headroom (don’t run Redis at 95% of box memory), enable vm.overcommit_memory=1, or move persistence to a replica.
Replication lag or broken sync. Symptoms: replicas far behind, stale reads, repeated full resyncs. Root cause: slow network, a too-small replication backlog forcing full resyncs after brief disconnects, or the primary too busy. Diagnose: INFO replication for master_repl_offset vs replica offset and master_link_status. Fix: enlarge repl-backlog-size, fix the network, reduce primary load.
Split-brain / lost writes on failover. Symptoms: after a network partition and failover, some acked writes are gone; briefly two nodes thought they were primary. Root cause (Core Idea 4): async replication + a partition let an isolated old primary accept writes that were discarded on reconciliation. Diagnose: correlate failover timing (Sentinel/Cluster logs: “subjectively down” → “objectively down” → election) with the missing writes. Mitigate: min-replicas-to-write + min-replicas-max-lag so an isolated primary stops accepting writes; odd number of primaries/Sentinels for clean quorum; accept that you cannot fully eliminate it without a CP system.
Connection exhaustion. Symptoms: rejected_connections climbing, clients timing out connecting. Root cause: app not pooling connections, or maxclients too low, or connections leaking. Diagnose: INFO clients, CLIENT LIST. Fix: use a connection pool in the app, raise maxclients, fix leaks.
The general debugging workflow. When something’s wrong and you don’t know what, in order: INFO (overall health — memory, clients, replication, persistence status), SLOWLOG GET 20 (is a slow command the culprit?), INFO memory + --bigkeys (memory pressure / big keys?), INFO replication (sync healthy?), CLIENT LIST (connection issues?), and MEMORY DOCTOR / LATENCY DOCTOR (Redis’s own diagnosis). Ninety percent of incidents are one of: a blocking command, memory pressure, or replication trouble — and those six checks find all three.
11. The Downsides / Disadvantages
The honest accounting. Redis is often still the right choice — but here’s what you’re signing up for.
1. Your data must fit in RAM, and RAM is expensive. Where it comes from: Core Idea 2 — memory-first is the whole premise. What it costs: you budget 1.5–2× your data size in RAM (headroom for fork-COW, replication buffers, fragmentation), and RAM costs far more per gigabyte than disk or SSD. A 200GB dataset that would be unremarkable in Postgres is an expensive, carefully-sharded Redis Cluster. Dealbreaker when: your working set is large and not hot — paying RAM prices to keep cold data in memory is waste. What people think mitigates it but doesn’t: “we’ll add more nodes” — Cluster spreads the cost across machines but doesn’t reduce it, and adds operational complexity on top.
2. Single-threaded means one core per instance for command processing. Where it comes from: Core Idea 1. What it costs: a single instance can’t use your 32-core box for command throughput; one slow command stalls all clients; scaling throughput means running many shards with all the routing and operational overhead that implies. Dealbreaker when: you have CPU-bound command patterns (heavy set operations, big Lua) at high concurrency on data that’s awkward to shard. What people think mitigates it but doesn’t: “Redis 6 added threading” — that’s network I/O only; command execution remains stubbornly single-threaded by design. (This is precisely the gap multi-threaded forks like KeyDB and rewrites like Dragonfly target.)
3. Durability is weak by default and never bulletproof. Where it comes from: Core Idea 2 + 4 — disk is an afterthought, replication is async. What it costs: out of the box you can lose seconds to minutes of writes; even appendfsync always can’t fully protect against power loss or storage corruption, and tightening durability tanks throughput. Dealbreaker when: Redis is your system of record for data you cannot reconstruct (financial ledgers, the only copy of orders). What people think mitigates it but doesn’t: “we enabled AOF, so it’s durable” — AOF reduces the window but Redis explicitly does not promise the durability or consistency of a real ACID database. Your Redis data should be reconstructible from somewhere else.
4. No strong consistency, by construction. Where it comes from: Core Idea 4 — async replication and best-effort failover. What it costs: acknowledged writes can be lost on failover; replicas serve stale reads; you cannot get linearizability through configuration. Building correctness-critical logic (inventory that must never oversell, locks that must never double-grant) on vanilla Redis is building on sand. Dealbreaker when: correctness under partition matters more than availability. What people think mitigates it but doesn’t: Redlock — genuinely contested and not a substitute for real consensus (Raft/Paxos systems) when safety is on the line.
5. Operational burden is real and ongoing. Where it comes from: memory-as-scarce-resource + fork-based persistence + failover topology. What it costs: you permanently watch memory pressure, fragmentation, big keys, eviction rates, fork pauses, replication lag, and failover health. Self-hosting Redis at scale is not “set and forget” — it’s a standing operational commitment, which is exactly why managed services exist and are popular. Dealbreaker when: you don’t have the ops maturity to monitor these and would be better served by a managed offering (and paying for it).
6. It’s a poor fit for large-value or query-heavy workloads. Where it comes from: it’s a data-structure server keyed by exact key, not a query engine. What it costs: no rich ad-hoc querying in core Redis (you bolt on the Query Engine module), no joins, and big values punish the single thread. If your access pattern is “find all records where X and Y and sort by Z,” that’s a database query, not a Redis lookup. Dealbreaker when: you need secondary indexes and flexible queries as the primary access pattern. Things it cannot do by construction: serve data larger than RAM, run truly concurrent command execution, or give you cross-shard transactions in Cluster mode.
7. The licensing saga cost trust, and the ecosystem fragmented. Where it comes from: a business decision, not a technical one — but it’s now a structural cost of choosing Redis. What it costs: in March 2024 Redis Ltd. abandoned the permissive BSD license for the dual SSPLv1/RSALv2 (neither OSI-approved), to stop cloud providers from monetizing Redis without contributing back. The community revolted, the Linux Foundation launched the BSD-licensed Valkey fork within weeks (backed by AWS, Google, Oracle), and major distros and managed services moved to it. Redis 8 (May 2025) added back the OSI-approved AGPLv3 and folded in the previously-proprietary modules — but AGPL’s network-copyleft has its own enterprise-legal friction, and many who left did not come back. What it costs you concretely: a real diligence decision (Section 8.10), AGPL copyleft considerations if you modify and serve Redis, and a split ecosystem where Valkey and Redis may diverge over time. Dealbreaker when: your legal team won’t touch AGPL or you need permissive licensing — in which case Valkey, not Redis, is your answer. (Redis 7.2.x remains BSD and can be used indefinitely if you freeze there.)
12. The Taste Test
What separates code that reveals understanding from code that was copied off the first tutorial.
Keys: namespaced and scannable vs opaque.
# Good — you can audit memory by function, scan by pattern, reason about ownership
cache:product:14523 session:a1b2c3d4 queue:email:pending lb:weekly:2026-25
# Bad — opaque, unscannable, un-ownable
14523 a1b2c3d4 tmpdata x
An experienced engineer’s keyspace tells you what the system does just by reading it.
TTLs: present and jittered vs absent. A cache SET with no EX is a red flag — that key lives forever and contributes to a slow OOM. Worse is a thousand keys all set with identical TTLs (synchronized stampede). Good code sets TTLs on all cache writes and adds ±10% jitter.
Reads: bounded vs unbounded. SCAN, HSCAN, ZRANGE start stop with real bounds = someone who knows the single thread is sacred. KEYS *, SMEMBERS huge_set, HGETALL huge_hash, LRANGE k 0 -1 in a hot path = someone who’ll cause an outage.
Atomicity: server-side vs read-modify-write. Incrementing a counter with GET then SET (a race) vs INCR (atomic). Doing conditional logic with WATCH/MULTI or a tight Lua script vs fetching to the app, deciding, and writing back across two round trips with a race in the middle. The taste: push the decision into Redis where it’s atomic.
Types: chosen for the job vs everything-is-a-string. Storing a user object as one JSON string you rewrite wholesale, vs a hash you update field by field. Sorting in app code, vs a sorted set whose score is the ordering. Using Pub/Sub for “reliable” messaging vs Streams when you need delivery and replay. Picking the structure whose native operations match your access pattern is the single clearest sign of fluency.
Persistence and memory config: deliberate vs default. maxmemory and maxmemory-policy explicitly set, persistence chosen to match the data’s value, cache and durable data on separate instances — vs running stock config and discovering the defaults during an incident. The config file is where taste is most visible because the defaults are tuned for “won’t surprise a beginner on a laptop,” not for production.
Connection handling: pooled vs per-request. A connection pool reused across requests vs opening a new connection per operation (which exhausts maxclients and adds handshake latency under load).
13. Where to Go Deeper
- The official docs at redis.io — genuinely good. Read the data-types pages, the “Redis at Scale” tutorial series (persistence, replication, scalability), and the persistence and eviction reference pages. Start here for anything operational.
- antirez’s blog (antirez.com) — Sanfilippo’s own writing on Redis internals, design decisions, and the Redlock debate. The closest thing to sitting with the creator. Read it when you want the why behind a design choice.
- “Redis in Action” by Josiah Carlson — the canonical book for patterns and idioms (rate limiters, queues, locks, autocomplete). Read it when you want to learn how to build with Redis, not just operate it.
- The Redis source itself (
redis/redison GitHub) —t_hash.c,t_zset.c,quicklist.c,listpack.c,server.c(the event loop). Smaller and more readable than most databases. Readdict.cand the listpack/intset files to truly understand encodings. The DeepWiki for the repo is a good guided map. - The Kleppmann–antirez Redlock debate (“How to do distributed locking” vs antirez’s reply) — read both when you’re tempted to use Redis for locking that matters. It’s the best real-world lesson in consistency tradeoffs you’ll find.
- The Valkey project (valkey.io) and its release notes — to understand where the open-source fork is going and how it differs (e.g., AWS’s async I/O threading contribution). Essential reading for the licensing/adoption decision in 2026.
- A hands-on project: build a rate limiter with a sorted set (sliding window), a work queue with
BRPOP, and a leaderboard with a zset — three small things that teach the three most important type choices. Then deliberately push a hash past its listpack threshold and watchOBJECT ENCODINGflip.
14. The Final Verdict
Here’s the honest take after all of that. Redis is one of the best-designed pieces of infrastructure software in wide use — not because it does the most, but because it does one thing with unusual clarity: it keeps your data structures in RAM and lets you manipulate them, fast, one atomic command at a time. The single-threaded model that sounds like a weakness on a slide is the source of its greatest virtues: free atomicity, predictable behavior, code you can actually reason about, and latency that’s boringly consistent until you do something dumb. Most “Redis is slow” incidents are really “I ran a blocking command and forgot there’s one thread.”
What it gets profoundly right: the data-structure-server idea itself. Exposing INCR, ZADD, and SADD over the network instead of opaque blobs collapsed a whole category of read-modify-write races and round trips that memcached forced on everyone. The size-dependent encodings are a quiet masterstroke — you get cache-friendly compactness for small things and proper algorithmic complexity for big things, automatically. And the API has taste: it’s small, orthogonal, and the commands compose. Few systems this powerful are this learnable in an afternoon.
What it costs you: everything traces back to “in RAM, one thread, async replication.” You pay RAM prices for your whole dataset, you get one core per shard, and you do not get strong durability or consistency without bolting on machinery that fights the design. The shape of the regret, if you feel it, is this: you reached for Redis because it was fast and easy, treated it as a database, and then a failover ate writes you thought were safe or an unbounded key froze the world. That’s not Redis betraying you — it’s Redis being exactly what it said it was, used as something it isn’t.
Who should reach for it: anyone who needs a fast, shared, in-memory home for caching, sessions, rate limiting, leaderboards, queues, real-time counters, or ephemeral coordination — which is to say, most backends. It’s the default right answer for “I need a cache” and a frequently-great answer for “I need a fast shared data structure.” Who shouldn’t: anyone who needs it to be the durable, consistent, queryable system of record for data they can’t reconstruct. Redis next to your real database, holding the hot and ephemeral state, is the sweet spot. Redis instead of your real database is where teams get burned.
What you should now believe. Believe that single-threaded is a feature and that every latency mystery starts at SLOWLOG. Believe that memory is the resource that governs everything and that maxmemory, eviction policy, and TTLs are day-one decisions, not tuning you do after the incident. Don’t believe Redis is durable or consistent by default — design as if it can lose recent writes, because it can. And when someone says “we’ll just use Redis for that,” the question to ask is always the same: what happens when this instance disappears for thirty seconds? If the answer is “we refill it,” Redis is perfect. If the answer is “we lose money,” you’re holding it wrong.
The hard-won line: Redis rewards you enormously for understanding that it has one thread and lives in RAM — and punishes you, eventually and at the worst possible time, for forgetting it.
The ideas are mine. The writing is AI assisted
Related reading
Rust Deep Intuition
An experienced engineer's guide to Rust
Go Ecosystem Deep Intuition
An experienced engineer's guide to the Go ecosystem
Postgres Deep Intuition
An experienced engineer's guide to Postgres
FastAPI Deep Intuition
An experienced engineer's guide to FastAPI