skip to content

Walk through one iteration of the Redis main event loop: how does a single thread serve tens of thousands of client connections, and what are file events versus time events?

level: middleimportance: should knowfreq 42%

answer

  1. ae wraps epoll / kqueue / evport / select
  2. file events vs time events
  3. beforeSleep: flush replies, AOF fsync
  4. serverCron at hz (default 10): expiry, rehash, evict
  5. non-blocking sockets, partial commands wait

basics

~20 s

Redis uses its own small event library (ae) over epoll on Linux or kqueue on BSD/macOS. Each iteration: run due time events, ask the kernel which sockets are readable/writable, then for each ready socket read, parse, execute and buffer the reply. Sockets are non-blocking, so the thread never waits on one client.

solid answer

~50 s

Redis ships its own event abstraction, `ae`, which wraps the best available multiplexer: `epoll` on Linux, `kqueue` on BSD/macOS, `evport` on Solaris, `select` as fallback. All sockets are non-blocking and registered with the multiplexer. One iteration: 1. `beforeSleep` runs housekeeping - flush pending client replies, fsync the AOF when policy requires, handle unblocked clients and cluster chores. 2. Call the multiplexer with a timeout computed from the nearest **time event**. 3. For each ready **file event**, run its handler: readable on the listening socket accepts a connection; readable on a client socket reads bytes, parses whole RESP commands, and executes them; writable drains the client's output buffer. 4. Fire due time events - mainly `serverCron`, which runs `hz` times per second (default 10) and does active expiry, incremental rehashing, replication and eviction bookkeeping. So concurrency comes from multiplexing readiness, not from threads: a connection costs a file descriptor and a buffer, not a thread.

code

text · 4 lines
text
hz 10                       # serverCron frequency (time events)
dynamic-hz yes              # scale hz with connected clients
maxclients 10000            # file descriptors the loop will multiplex
client-output-buffer-limit pubsub 32mb 8mb 60   # kill slow consumers

go deeper

for a junior

Know that Redis multiplexes many sockets with epoll/kqueue on one thread instead of one thread per client, and that sockets are non-blocking.

for a middle

Be able to narrate an iteration: beforeSleep, poll with a timeout, handle readable/writable file events, then run serverCron time events.

for a senior

Connect the loop to operations: hz, output-buffer limits and incremental rehash/expiry all exist to keep any single iteration bounded.

for a principal

Discuss the loop as an admission-control and latency-budget surface - what work is allowed on it, what is deferred to bio threads, forks or io-threads, and why.

## The core idea Redis never dedicates a thread or process per connection. Instead, all sockets are set non-blocking and handed to the kernel's readiness API. The kernel tells Redis *which* descriptors can be read or written right now; Redis services exactly those and nothing else. That is why one thread can hold 50,000 idle connections cheaply: an idle connection costs a file descriptor and a client struct, not a stack. ## ae, and what it wraps `ae` ("a simple event-driven programming library") is Redis's own ~1000-line abstraction, chosen so Redis has no external dependency. At compile time it selects the best backend available: `epoll` (Linux), `kqueue` (BSD, macOS), `evport` (Solaris), falling back to `select`. `epoll`/`kqueue` matter because they are O(ready) rather than O(watched): with `select` the server would rescan every descriptor on every pass. `ae` knows two kinds of events: - **File events** - a descriptor became readable or writable. These carry the actual client work. - **Time events** - a callback that should fire at or after some time. In practice, one dominant time event: `serverCron`. ## One iteration, step by step **1. beforeSleep.** Before blocking in the kernel, Redis runs a hook: write out replies that are pending for clients (so a reply produced this iteration usually leaves in the same iteration), fsync the AOF buffer if `appendfsync everysec` says it is due, process clients unblocked by a `BLPOP`-style wakeup, handle cluster and replication chores, and, in newer versions, dispatch work to `io-threads`. **2. Poll.** Redis computes how long it may sleep - the interval until the nearest time event - and calls `epoll_wait`/`kevent` with that timeout. If nothing is ready and no timer is due, the thread sleeps in the kernel and burns no CPU. **3. Process file events.** For each ready descriptor Redis calls its handler: - The **listening socket** readable means a new connection: accept it, set it non-blocking, allocate a client, register a read handler. - A **client socket** readable means bytes arrived: read into the query buffer, parse as many *complete* RESP commands as the buffer holds (a partial command simply waits for more bytes; there is no blocking read), then execute each one and append the reply to the client's output buffer. - A **client socket** writable means Redis can drain a reply that did not fit earlier - typical for large replies or slow consumers. **4. Process time events.** `serverCron` runs `hz` times per second (default `hz 10`, i.e. every 100 ms; `dynamic-hz yes` scales it with client count). It performs the periodic work that has to happen regardless of traffic: sampling keys for **active expiry**, incremental **rehashing** of the main dictionary, resizing, checking replication and failover state, evicting when `maxmemory` is exceeded, updating stats, and triggering `BGSAVE`/AOF rewrite when the configured thresholds are met. Then the loop repeats. ## Consequences worth naming **Fairness is per-iteration, not per-client-fairness in any strict sense.** Redis limits how much it reads from one client per pass and caps replies it will buffer, but a single expensive *command* still runs to completion - the loop is cooperative, not preemptive. **Reply buffering matters.** Because writes are also event-driven, a slow or stalled consumer accumulates memory in its output buffer. That is why `client-output-buffer-limit` exists, with distinct budgets for normal clients, replicas and pub/sub subscribers - Redis will disconnect a client that grows past its limit rather than let it exhaust server memory. **Periodic work is amortized, never a stop-the-world pass.** Both expiry and rehashing are deliberately incremental so `serverCron` does a bounded amount of work per invocation and hands the loop back. **`hz` is a latency-vs-CPU knob.** Raising it makes expiry and background bookkeeping more responsive at the cost of more wakeups; it is rarely worth changing without evidence. **The kernel is the concurrency engine.** All the parallelism Redis exploits at this layer is the kernel's ability to say "these 200 of your 50,000 sockets are ready". Everything after that is a fast serial pass.

  • What happens if a client sends only half of a command and then goes quiet?
    The bytes sit in that client's query buffer and the loop moves on; nothing blocks. When the rest arrives the read handler fires again, the command parses completely and executes. A client that keeps sending an unbounded partial command is bounded by the query-buffer limit, and Redis will close it rather than grow memory forever.
  • Why is dictionary rehashing incremental rather than done in one pass?
    Because a full rehash of a multi-million-key dictionary would occupy the single thread for a long time and spike latency for every client. Redis keeps two tables during a rehash and migrates a few buckets per command and per serverCron tick, so the cost is spread out and no single iteration stalls the loop.

A receptionist with a board of call lights instead of one phone per line. She never waits on a silent line - she looks at which lights are lit, handles those, and glances at the clock for scheduled chores.

saying these in an interview costs you the question

  • Thinking Redis spawns a thread or process per connection
  • Believing epoll makes commands run concurrently rather than just reporting readiness
  • Assuming a blocking read is used, so a slow client could freeze the server
  • Saying serverCron expires all expired keys in one pass
  • Confusing high connection counts with high CPU cost when connections are idle

context