Lesson 0001 · Internals & Performance
Why Redis runs your commands on one core — and the single failure mode that model creates.
You already use Redis. This lesson turns a fact you've half-heard — "Redis is single-threaded" — into the keystone mental model this whole course hangs off. Get this one idea and its consequences derive themselves: why Redis is fast, why your commands are atomic without you asking, why a command's big-O is really a shared-latency decision, and why one careless command can freeze the entire server for every client at once.
Redis executes your commands one at a time, on a single thread, driven by an event loop. Thousands of clients can be connected, but their commands are serialized into one queue and run to completion back-to-back — never in parallel. — Redis Docs: "Redis is, mostly, a single-threaded server from the point of view of commands execution"
The loop itself is built on the OS's readiness API — epoll on Linux,
kqueue on BSD/macOS. That's what lets one thread watch tens of thousands
of sockets cheaply: the kernel reports which connections have data ready, and Redis
services them in a tight cycle.
— Redis Docs — benchmarks
One thread for command execution is not a limitation Redis tolerates — it's a design choice. At in-memory speeds the CPU is rarely the bottleneck; memory and network bandwidth are. A single thread buys simplicity and determinism and sidesteps the lock contention that would otherwise eat the gains of adding cores.
Because commands run one-at-a-time to completion, each one is atomic
by construction — no other command can interleave halfway through. This is why
INCR can't lose an update under concurrency, and why you never reach for a
lock to protect a single Redis operation. The serialization is the mutual
exclusion.
A single Redis instance won't use your other cores no matter how many you have. For raw throughput you scale by running multiple instances / shards (one process per core), which is exactly what Redis Cluster formalizes — a thread later in this course. — Redis Docs — benchmarks
Here is the sharp edge, and the reason this lesson comes first. If one command is slow, every other client waits behind it. There is one queue and one worker; a slow job at the front stalls the whole line. This is head-of-line blocking, and it is the root cause of most "Redis got slow for no reason" incidents. — Redis Docs — Diagnosing latency issues
So a command's time complexity is not academic. On a shared single thread, big-O is how long you hold the lock on the entire server. Every command page on redis.io lists its complexity precisely for this reason:
| Command | Complexity | Shared-thread reading |
|---|---|---|
SET k v | O(1) | Constant — safe at any scale. |
HSET h f v | O(1)/field | Constant per field. |
ZADD z s m | O(log N) | Cheap even on huge sorted sets. |
LRANGE l 0 -1 | O(S+N) | Scales with elements returned — a big list blocks everyone. |
KEYS * | O(N) over all keys | Scans the entire keyspace on the main thread. Never in production. |
Complexity strings are quoted from each command's redis.io page —
e.g. ZADD is
O(log(N)) for each item added.
KEYS * on a large keyspace, LRANGE 0 -1 on a million-element
list, SMEMBERS on a huge set, or DEL on a giant collection —
each is a single O(N) command that monopolizes the one thread. Latency for
every client spikes until it finishes.
SCAN (and
HSCAN/SSCAN/ZSCAN) — cursor-based, O(1) per
call — instead of KEYS/SMEMBERS.UNLINK instead of
DEL — it reclaims memory in a background thread.Redis has always used background threads for some work — persistence
happens in a child process via fork(), and lazy-freeing
(UNLINK, async delete) and some fsync run off the main thread.
Since Redis 6.0, optional threaded I/O can read/parse
requests and write replies on multiple threads. But in every version, the actual
execution of a command against your data still happens on the one main
thread. "Single-threaded" is a statement about command execution, not the whole process.
Answer from memory — the effortful recall is what builds retention. Feedback is immediate.
Why can two clients' INCR on the same key never lose an update?
On a single Redis instance, the usual throughput bottleneck is:
Running KEYS * on a large keyspace mainly hurts because it:
On the shared single thread, a command's time complexity is really a measure of:
"Redis is single-threaded" most precisely means that Redis runs on one thread its:
Recall is one thing; watching the one thread block is another. You need a
local Redis (redis-server, or docker run --rm -p 6379:6379 redis).
You'll open two redis-cli sessions.
redis-cli --latency
# leave it running — it PINGs continuously and prints min/avg/max ms
redis-cli DEBUG SLEEP 3
# blocks the ENTIRE server inside command execution for 3 seconds
Before you run it: what happens to the --latency numbers in the first
terminal during those 3 seconds? Commit to an answer, then watch.
The --latency monitor stalls, then reports a
max around 3000 ms. A trivial PING — O(1),
normally sub-millisecond — was stuck behind one slow command on the shared thread.
That gap is head-of-line blocking, made numeric.
# seed some keys
redis-cli EVAL "for i=1,100000 do redis.call('set','k:'..i,i) end" 0
redis-cli --latency & # watch latency
redis-cli KEYS 'k:*' > /dev/null # one O(N) blast — spikes the monitor
redis-cli --scan --pattern 'k:*' > /dev/null # cursor-based, stays smooth
When you're done, redis-cli FLUSHALL to clean up — and bring your
observed max latency to your teacher if anything surprised you.
Redis Docs — Benchmarks ("How fast is Redis?"). It states the single-threaded model and the epoll/kqueue event loop in the authors' own words, and is the reference these claims trace back to. Follow it with Diagnosing latency issues for the failure modes.