Skip to main content
Articles/redis/how-redis-internals-work
Scenario Article

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

Abstract Algorithms Database Teamβ€’Oct 7, 2026β€’Evidence & Source β†—
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1: Epoll Event Registration & Ingress Dispatch

Action / Decision:

Client sends GET cache:user:1001 command over TCP socket. Linux epoll_wait signals readable file descriptor.

Component Behavior:

Redis main thread wakes from epoll_wait, reads socket payload into client buffer, and parses RESP protocol.

Observable Output: Command parsed as GET in 12 microseconds; key lookup dispatched to dictFind().
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit