The idea: many connections, one command thread

A Redis server often has thousands of clients connected at once. It does not start a thread per client. One main thread runs an event loop: it asks the kernel which connections have data, reads them, runs the commands one after another, and writes the replies back. Networking involves many connections; command execution happens in one place, in one order.

Clients A, B and C each have a TCP connection to a kernel socket, fd 7, 8 and 9. The ready sockets are reported by epoll_wait to the single main thread, which loops through read, parse, execute, addReply, write and back to epoll_wait, one command at a time
Every connection has its own socket, but one main thread runs the event loop and executes all commands, one at a time.

The page has four clients. A, B and C are connected at the start; D is not. Pick a client and a command, press Send (the bytes reach the server), then Run Loop (one turn of the event loop). Real Redis wakes up as soon as data arrives; here it waits for you, so several clients can be ready at the same time.

A connection: a socket and a client struct

Each client has its own TCP connection. In the kernel that is a socket with a receive buffer (bytes from the client, not read yet) and a send buffer (bytes to the client, not sent yet). Redis knows the socket by its file descriptor (fd 7, 8, 9 on the page) and keeps a client struct for it with a query buffer (querybuf, raw bytes read from the socket), the parsed command (argv) and a reply buffer.

New connections arrive on the listening socket (fd 6, port 6379). The kernel finishes the TCP handshake by itself and queues the connection; Redis picks it up with accept4(), which returns the lowest free fd, registers that fd with epoll_ctl(ADD) and creates the client struct. QUIT or a closed connection reverses this: close(fd) also removes the fd from epoll. (Demo 7: open and close a connection)

One turn of the event loop

StageRedis functionWhat happens
epoll_waitaeProcessEventsThe thread sleeps until some sockets are readable, then gets the list of ready fds.
readreadQueryFromClientOne read() copies all the waiting bytes into the client's querybuf.
parseprocessInputBufferThe RESP bytes (*3 $3 SET $2 k1 $2 v1) become argv = [SET, k1, v1].
executeprocessCommand → call()The command runs against the keyspace.
addReplyaddReplyThe reply goes into the client's reply buffer; the client goes on clients_pending_write.
writebeforeSleep → handleClientsWithPendingWritesJust before sleeping again, write() sends each pending reply.

Replies are not written the moment a command finishes. They are collected and written in beforeSleep, so one turn of the loop handles all ready clients and then flushes all replies. If a socket's send buffer is full, Redis writes what fits and asks epoll to tell it when the socket is writable again. (Demo 1: three clients, one thread)

Why no locks are needed

Only the main thread touches the keyspace, and it runs one command at a time from start to end. So every single command is atomic: two clients that both run INCR counter always end with 2, never 1 (Demo 3). The data structures need no locks, which keeps them simple and fast.

The order is the order the loop reads the commands. Commands that arrive at about the same time from different clients can run in either order: a GET k1 that is read before the SET k1 v1 gets (nil) (Demo 2: arrival order decides). Only commands on the same connection have a guaranteed order. For several commands that must run together, use MULTI/EXEC or a Lua script.

Pipelining

A client does not have to wait for each reply before sending the next command. With pipelining it writes several commands at once; Redis reads them with one read(), runs them in order, and sends all replies with one write(). That saves network round trips and system calls (Demo 4: 1 read and 1 write instead of 3 of each). Pipelining is not atomic: commands from other clients can run between them.

The cost: one slow command blocks everyone

One thread means one queue. While the main thread runs a slow command, nobody else is served, not even a cheap GET. KEYS * walks the whole keyspace; on a large database that takes seconds. In Demo 5 it takes 8 ticks, and clients A and B wait behind it.

Timeline of the main thread: a read, then KEYS * for 8 ticks, then the two GETs, then the write. Below, client A's GET k1 and client B's GET k2 wait during the whole KEYS * before they run
While KEYS * runs for 8 ticks, the cheap GETs of A and B just wait: one slow command delays every client.
  • Use SCAN instead of KEYS: it returns the keys a few at a time, across many short commands.
  • Use UNLINK instead of DEL for big keys: the memory is freed by a background thread.
  • Find slow commands with SLOWLOG GET and LATENCY DOCTOR.

io-threads: parallel I/O, serial execution

With many clients, a lot of the main thread's time goes into read(), parsing and write(), not into the commands themselves. Redis 6 added io-threads: with io-threads 3 and io-threads-do-reads yes, the ready clients are dealt out to the main thread and two I/O threads, which read and parse in parallel; after the main thread has executed the commands, the replies are written in parallel too. Execution stays on the main thread, one command at a time, so everything in the previous sections still holds. (Demo 6)

io-threads 1 (default)io-threads 3
read + parsemain thread, one client after anothermain + 2 I/O threads, in parallel
executemain thread, in ordermain thread, in order
writemain thread, one client after anothermain + 2 I/O threads, in parallel
Demo 1/6: one loop turn9 ticks5 ticks

Redis also has background threads (bio) for slow work that does not touch the keyspace in order: freeing memory for UNLINK, fsync of the AOF file, closing files. Forked child processes write RDB snapshots and rewrite the AOF.

See also Linux epoll for what epoll_wait does inside the kernel, the Node.js event loop for the same single-threaded design in JavaScript, how nginx handles a request for an event loop per worker process, and Redis Cluster for spreading keys over many such servers.

What the page leaves out

Network delay (packets arrive at once) and the real wake-up timing; the RESP3 protocol and inline commands; partial reads and big replies that need several write() calls; client output buffer limits and maxclients; blocking commands such as BLPOP; MULTI/EXEC, Lua and functions; Pub/Sub; replication and persistence traffic; TLS; timers (serverCron); the rule that Redis only uses I/O threads when enough clients are pending; and Redis 8's reworked I/O threads, where each I/O thread owns a set of connections.