Grokking Redis
Vote

0% completed

​

1.1 Why Redis Is Fast

In-Memory Storage

The Latency Gap

Data Structures Designed for Memory

Durability

The Single-Threaded Event Loop

One Thread for All Commands

How the Loop Works

The Cost of the Alternative

Atomicity Without Locks

No Locks, Predictable Latency

What Locks Cost

Predictability Over Peak Throughput

Slow Commands Block Every Client

Using This in an Interview

FAQs

Key Takeaways

Most databases get faster by adding threads, cores and parallel workers. Redis does the opposite: it runs every command on a single thread, and one instance still serves on the order of 100,000 operations per second on ordinary hardware. With pipelining, which the next lesson explains, many benchmarks reach about a million operations per second, and typical latencies are measured in microseconds.

This result follows from a chain of design decisions in which each one makes the next possible. Data lives in memory, so each operation is cheap. Cheap operations mean one thread can execute all of them, and one thread means the data needs no locks. Without locks there is no waiting between clients, so latency stays predictable. When profiling later showed that the real bottleneck was the network, Redis added threads for socket reading and writing, and command execution stayed on one thread.

This lesson follows one example through the chain up to predictable latency, and the next lesson covers the I/O threads. The example is an online shop whose Redis instance stores a page view counter under the key page:views and user profiles under keys such as user:42.

In-Memory Storage

The Latency Gap

The first reason is the simplest and the largest. Main memory access takes roughly 100 nanoseconds, while a random read from an SSD takes roughly 100 microseconds, which is a thousand times longer. A spinning disk seek takes around 10 milliseconds, a hundred thousand times longer than memory.

A disk-based database spends much of its effort hiding this gap with buffer pools, caching layers and careful page layout. Redis removes the gap by keeping the entire dataset in RAM. When the shop reads user:42, no disk is involved at any point.

Redis vs. Disk-Based Databases: The Latency Gap
Redis vs. Disk-Based Databases: The Latency Gap

Data Structures Designed for Memory

Because nothing has to fit into disk pages, Redis uses structures designed for memory. Keys and hashes live in hash tables, which find an entry in constant time, O(1). Sorted sets use a skip list, which is a sorted linked list with extra "express" pointers that let a search skip ahead, so a lookup takes O(log N) steps. Small collections use a listpack, a single block of memory that stores the elements one after another with very little overhead. Special types such as HyperLogLog and bitmaps cover counting problems.

A lookup is a chain of pointer reads in RAM, and there is no conversion between an on-disk format and an in-memory one. A value is read and written in the same form it is stored in, so INCR page:views changes an integer in place.

Redis In-Memory Data Structures
Redis In-Memory Data Structures

Durability

Memory loses its contents when the process stops, so Redis offers persistence that stays off the path of each request. An RDB snapshot is written periodically by a child process that Redis creates with fork. The child and the parent share the same memory pages, and the operating system copies a page only when the parent modifies it (a technique called copy-on-write), so the parent keeps serving requests while the child writes.

The append-only file (AOF) logs every write command. Its fsync policy decides how often Redis asks the operating system to force the log onto disk (fsync is that system call). everysec is the default and the common compromise, and it runs the fsync on a background thread, so a crash can lose about one second of writes. always is the safest and the slowest, and no leaves flushing to the operating system. Appending to a log is sequential I/O, which is cheap compared with random page writes, so you choose explicitly how much recent data you can afford to lose in exchange for speed.

The Single-Threaded Event Loop

One Thread for All Commands

Once data is in memory, a typical command such as GET, SET or INCR needs about a microsecond of CPU work or less. A server that does a microsecond of work per command is rarely limited by computation. It is limited by reading requests from sockets and writing replies to them.

At about a microsecond per command, one thread can in principle execute on the order of a million small commands per second. More threads executing commands would mostly add coordination cost, because they would need to agree on who may touch page:views at any moment.

How the Loop Works

Redis handles all of its clients with an event loop, a single thread that repeatedly asks the operating system which connections have work and then serves those connections. The question is answered by the operating system's I/O multiplexing interface, which tells one thread which of thousands of sockets are ready: epoll on Linux and kqueue on BSD and macOS. Each turn of the loop runs in these steps:

  1. The loop asks the kernel which client sockets are ready.
  2. For each ready socket, it reads the request and parses the command.
  3. It executes the command against the in-memory data.
  4. It queues the reply to be written back to the client.
  5. It handles timers, such as expiring keys and other background housekeeping, and then starts the next turn.

All sockets are non-blocking, so the thread never waits on one slow client. Node.js and Nginx use the same model. A connection costs only an entry in a table rather than a thread with its own stack, which is how one thread can hold tens of thousands of concurrent connections.

How Redis's Single-Threaded Event Loop Works
How Redis's Single-Threaded Event Loop Works

The Cost of the Alternative

A design with one thread per connection pays for thread stacks, context switches between threads and the work of the operating system scheduler. Most of all, it pays for the synchronization that protects shared data from two threads changing it at the same time. Each of those costs is larger than the microsecond of useful work that a Redis command performs, so for work this small, parallel execution is a net loss.

Atomicity Without Locks

Because only one command runs at a time, every command is atomic without any extra mechanism. Two clients that each send INCR page:views at the same moment always produce two separate increments, because the second INCR starts only after the first has finished. A list push cannot be left half done, and a MULTI and EXEC transaction or a Lua script runs as one block that no other client can observe halfway through. As a result, developers get serialized behavior, which removes whole categories of race conditions from application code.

No Locks, Predictable Latency

What Locks Cost

In a multithreaded database, shared structures are guarded by locks. Even an uncontended lock costs atomic CPU instructions and traffic between the caches of different cores. When two threads want the same lock, one of them blocks, the scheduler gets involved, and a request that normally takes microseconds may stall for milliseconds.

Lock problems such as convoys, priority inversion and deadlocks are hard to reason about. They also produce tail latency, which means the response time of the slowest small fraction of requests. It is usually measured as a percentile: p99 is the time within which 99 percent of requests complete, so 1 request in 100 is slower. At scale, many users meet that slowest 1 percent every day.

Single-Threaded vs. Multi-Threaded Execution: Why Redis Avoids Locks
Single-Threaded vs. Multi-Threaded Execution: Why Redis Avoids Locks

Predictability Over Peak Throughput

Redis avoids these problems because only one thread touches the keyspace, so the keyspace needs no lock. Latency is therefore determined by the work the command itself does, not by what other clients are doing. As long as the commands are cheap, p99 latency stays close to the median.

The single thread also keeps the engine simple. Data structures need no concurrent variants, memory management is straightforward, and there are fewer subtle bugs. Simple code paths are easier to make fast and easier to keep fast, so simplicity is itself a performance feature.

Slow Commands Block Every Client

The same property is the main risk of the design. All commands share one thread, so a slow command delays every other client. Commands such as KEYS on a large database, SMEMBERS or HGETALL on a huge collection, a long Lua script, or DEL on a key that holds millions of elements can block the server for hundreds of milliseconds or even seconds.

Suppose one client runs KEYS user:* on the shop's instance and it takes 200 milliseconds. Every GET for user:42 that arrives during that time waits up to 200 milliseconds for a reply that normally takes a few microseconds. The standard practices follow from this. Use SCAN, which walks the keyspace in small steps, instead of KEYS, and read large collections in pieces. Use UNLINK, which frees memory on a background thread, rather than DEL for big keys. Keep scripts short, and watch the slow log, which records commands that ran longer than a configured threshold.

How One Slow Redis Command Blocks Other Clients
How One Slow Redis Command Blocks Other Clients

Several internal techniques keep each turn of the loop short for the same reason. Expired keys are removed partly on access and partly by a background cycle that works in small batches with a time limit. The main hash table is rehashed incrementally, a few buckets at a time, rather than all at once. Persistence runs in a forked child, so each of these choices avoids a long pause on the main thread.

Using This in an Interview

Explain the chain in order: data in memory makes each command cost about a microsecond, so one thread can execute everything, so the keyspace needs no locks, which makes commands atomic and latency predictable.

What is the weakness of this design? One slow command, such as KEYS or DEL on a huge key, blocks every client, so you use SCAN, UNLINK and bounded reads.

Why are Redis commands atomic without locks? Only one command runs at a time, so two INCR calls on the same key always produce two separate increments.

FAQs

If Redis executes commands on one thread, how can it handle thousands of clients?

Connections are multiplexed with epoll or kqueue, and the sockets are non-blocking. The thread only works on sockets that are ready, so a slow client costs nothing while it is idle. Because each command takes microseconds, one thread can serve a very large number of connections in quick rotation.

Why not use multiple threads to execute commands?

The commands are so cheap that synchronization would cost more than the work. Locks, contention between core caches and context switches would slow common operations, add tail latency and complicate the engine, and they would remove the atomicity that serial execution provides without extra effort. Redis uses more cores by sharding with Redis Cluster instead.

Why can a single command slow down the whole server?

All clients share the one execution thread, so a command that runs for 200 milliseconds makes everyone wait up to 200 milliseconds. Avoid KEYS, unbounded reads of large collections, long scripts and synchronous deletion of huge keys. Use SCAN, UNLINK and smaller batches instead.

Does persistence slow Redis down?

Snapshots use fork and copy-on-write, so the main thread continues, although a fork on a large instance can cause a short pause and uses extra memory. The append-only file adds sequential writes, and the fsync policy sets the trade-off: everysec is the default and risks about one second of writes, while always is slower but loses the least.

Key Takeaways

  • Memory is the foundation. RAM access is about a thousand times faster than an SSD read, and Redis stores data in structures designed for memory.
  • Commands are small, so one thread is enough. At about a microsecond of work per command, coordination between threads would cost more than it saves.
  • The event loop multiplexes many connections. Non-blocking sockets with epoll or kqueue let one thread serve thousands of clients.
  • No locks means predictable latency. Serial execution removes contention and tail-latency spikes, and it makes every command atomic.
  • The same design makes slow commands dangerous. One long command blocks all clients, so use SCAN, UNLINK, short scripts and bounded reads.
  • Persistence stays off the request path. Forked snapshots and a configurable AOF fsync policy trade durability for speed explicitly.

Reading Progress

0%


Vote for new content

On This Page

In-Memory Storage

The Latency Gap

Data Structures Designed for Memory

Durability

The Single-Threaded Event Loop

One Thread for All Commands

How the Loop Works

The Cost of the Alternative

Atomicity Without Locks

No Locks, Predictable Latency

What Locks Cost

Predictability Over Peak Throughput

Slow Commands Block Every Client

Using This in an Interview

FAQs

Key Takeaways