Lesson 0001 · Internals & Performance

The single-threaded event loop

Why Redis runs your commands on one core — and the single failure mode that model creates.

~8 minRetrieval quiz at the endCheat sheet: here

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.

The one idea

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

The key insight

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.

Two consequences you get for free

1. Every command is atomic

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.

2. The bottleneck is memory and network, not CPU

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

The cost: head-of-line blocking

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:

CommandComplexityShared-thread reading
SET k vO(1)Constant — safe at any scale.
HSET h f vO(1)/fieldConstant per field.
ZADD z s mO(log N)Cheap even on huge sorted sets.
LRANGE l 0 -1O(S+N)Scales with elements returned — a big list blocks everyone.
KEYS *O(N) over all keysScans 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.

The classic footgun

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.

The fixes (preview)

Interview-grade nuance: what "single-threaded" does not mean

It's the command loop that's single

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.

Check yourself

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?

Serialized execution on one thread makes each command atomic — the interleaving that would cause a lost update simply can't happen. No explicit lock needed.

On a single Redis instance, the usual throughput bottleneck is:

One thread won't use extra cores, so CPU is rarely the limit. Redis is memory- and network-bound; you scale throughput by running more instances/shards.

Running KEYS * on a large keyspace mainly hurts because it:

It's one O(N) command on the shared main thread — head-of-line blocking. Every other client waits until the full keyspace scan finishes. Use SCAN instead.

On the shared single thread, a command's time complexity is really a measure of:

Big-O is how long the command holds the one worker thread — i.e. the latency it imposes on every other client. That's why redis.io lists complexity on every command.

"Redis is single-threaded" most precisely means that Redis runs on one thread its:

Command execution is single-threaded. Persistence (fork), lazy-freeing, and — since 6.0 — optional threaded I/O run elsewhere. The data-touching step is the single one.

Hands-on: freeze the server on purpose

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.

1 · Baseline latency

redis-cli --latency
# leave it running — it PINGs continuously and prints min/avg/max ms

2 · In a second terminal, hold the thread

redis-cli DEBUG SLEEP 3
# blocks the ENTIRE server inside command execution for 3 seconds
Predict first

Before you run it: what happens to the --latency numbers in the first terminal during those 3 seconds? Commit to an answer, then watch.

What you should observe

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.

3 · Contrast SCAN vs KEYS

# 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.

Primary source — read this next

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.