Redis Single-Threaded Event Loop & Epoll: High-Throughput I/O Architecture
How Redis handles 100,000+ QPS without multi-threading lock contention using Linux epoll and non-blocking sockets
Trace how Redis Event Reactor multiplexes thousands of concurrent TCP client connections through a single main thread loop, avoiding thread synchronization overhead.
π― System Scenario & Requirements
System Goal
Serve 120,000 read/write cache queries per second on a single CPU core with sub-millisecond p99 latency.
Actors & Components
- Client API Servers
- Linux epoll Event Loop (AE Event Reactor)
- Single Main Worker Thread
- In-Memory Hash Table Dictionary
Functional Requirements
- Zero Lock Contention: Eliminates mutexes and context switches.
- Non-Blocking Sockets: Read and write operations never block main loop execution.
- Sub-Millisecond p99: Latency stays below 0.8ms under 80% CPU utilization.
Constraints & Assumptions
- Any long-running O(N) operation (e.g. KEYS *) blocks all incoming client traffic.
β‘ Execution Stages & Decision Trace
Stage 1: Epoll Event Registration & Ingress Dispatch
Client sends GET cache:user:1001 command over TCP socket. Linux epoll_wait signals readable file descriptor.
Redis main thread wakes from epoll_wait, reads socket payload into client buffer, and parses RESP protocol.
Deep Dive
One thread must serve thousands of cache clients without blocking
A product API depends on Redis for session and catalog caching. At peak, tens of thousands of TCP connections send sub-millisecond GET and SET commands. Spawning a thread per connection would waste memory on stacks and context switches. Redis instead multiplexes all client I/O through a single event loop on one core.
This scenario follows one GET command from socket readability through dictionary lookup to asynchronous reply buffering.
sequenceDiagram
title Redis Single-Threaded Event Loop Dispatch
participant Client as API client
participant Loop as Redis main thread
participant Epoll as Linux epoll
participant Dict as In-memory dict
Client->>Loop: GET cache:user:1001
Loop->>Epoll: wait for readable socket
Epoll-->>Loop: fd ready
Loop->>Dict: dictFind(key)
Dict-->>Loop: value pointer
Loop-->>Client: RESP reply buffered
Stage 1 β Register sockets and wake on readability
When a client connects, Redis registers the socket with epoll in non-blocking mode. The main thread blocks in epoll_wait until at least one descriptor is ready. caching is the storage model this loop serves: every ready event eventually becomes a key lookup or mutation.
- No thread is dedicated to a single client; readiness drives scheduling.
- Edge-triggered or level-triggered epoll behavior must match the read loop implementation.
- Slow clients cannot block others because sockets are non-blocking.
Observable outcome: The process wakes only when work exists, keeping CPU focused on protocol parsing and command dispatch.
Stage 2 β Parse RESP and dispatch the command
The main thread reads available bytes, parses the Redis Serialization Protocol (RESP), and maps the command to a handler. For GET cache:user:1001, the handler performs a dictionary lookup and prepares the reply in the client output buffer.
rate-limiting appears indirectly through TTL and eviction policies that decide whether a key is still valid when the lookup succeeds.
Observable outcome: The command is parsed and executed without yielding the main thread to another client mid-command.
Stage 3 β Buffer replies and register writable events
If the kernel send buffer is full, Redis queues the response and registers a writable epoll event. The same thread later drains the output buffer when the socket accepts more data.
Observable outcome: Clients receive responses with predictable latency until a blocking command stalls the loop.
End-to-end outcome
Redis sustains very high QPS on one core because it avoids lock contention and schedules by I/O readiness. Operators trade horizontal sharding for per-instance throughput when a single hot key or blocking command appears.
Alternatives considered
Multi-threaded cache (Memcached-style)
Uses multiple cores per process but pays for fine-grained locking on shared structures.
Failure and degraded modes
Blocking O(N) commands
Commands such as KEYS * monopolize the main thread and stall every other client until the scan completes.
Key learnings
- Redisβs single-threaded command execution model buys simplicity and low latency, but any blocking O(N) command stalls every client on that instance.
- The event loop multiplexes sockets; throughput comes from non-blocking I/O plus fast in-memory structures, not from one thread per connection.
- Persistence (RDB/AOF) and replication are separate durability/HA choices β neither is free with respect to latency or failover semantics.
- Hot keys and large values dominate p99 more often than raw ops/sec averages suggest; design keys and commands for that reality.
- Prefer bounded commands and pipelining over KEYS-style scans in production paths.
