Open-Source Wikis

/

Redis

/

Systems

/

IO threads

redis/redis

IO threads

Active contributors: antirez, Oran Agra, debing.sun, zhaozhao.zz.

Purpose

The main thread runs every command. Reading and writing socket buffers, however, can be parallelised — bytes go to and from sockets independently. The IO threads subsystem (src/iothread.c) lets the server use additional OS threads to drain query buffers and fill output buffers, leaving the main thread to dispatch commands.

This is not multi-threaded command execution. The dataset still has one writer.

Source layout

File Role
src/iothread.c The IO thread implementation: per-thread queues, main-thread handoff, the IOThread struct and worker loop. ~41 KB.
src/eventnotifier.c, src/eventnotifier.h Cross-thread wakeup primitive. eventfd on Linux, pipe elsewhere.
src/threads_mngr.c Cross-thread stack capture for the watchdog and DEBUG STACK-TRACE.
src/bio.c A separate, older threading subsystem with three named workers (BIO_CLOSE_FILE, BIO_AOF_FSYNC, BIO_LAZY_FREE).

Configuration

io-threads 4                      # default 1 means main-thread only
io-threads-do-reads yes           # also offload reads (writes are always offloaded)

io-threads is the count including the main thread. With io-threads 4 you get 1 main + 3 workers.

Architecture

graph TD
    Main[Main thread<br/>command dispatch]
    Main -->|hand off pending writes| W1[IO Thread 1]
    Main -->|hand off pending writes| W2[IO Thread 2]
    Main -->|hand off pending writes| W3[IO Thread 3]
    W1 -->|done| Notify1[eventNotifier]
    W2 -->|done| Notify2[eventNotifier]
    W3 -->|done| Notify3[eventNotifier]
    Notify1 --> Main
    Notify2 --> Main
    Notify3 --> Main

Each IO thread owns a list of clients it is currently servicing. When the main thread wants to offload work, it pushes the client onto the thread's pending_clients_to_io_threads list and triggers the thread's eventNotifier. The thread wakes up, processes the queue (writeToClient or readQueryFromClient), then pushes the client back onto a pending_clients_to_main_thread list and triggers the main thread's notifier.

The actual command logic — processCommand and the type implementations — runs only on the main thread.

What gets offloaded

Phase Main thread? IO thread?
Accept new connection Yes (always) No
read(2) query bytes Optionally Yes if io-threads-do-reads yes
RESP parse Optionally Yes (in the IO thread)
processCommand Yes (always) No
Build reply (addReply*) Yes No
write(2) reply bytes Sometimes Yes
Free client on disconnect Yes No

Reads are gated by io-threads-do-reads because parsing in the IO thread requires the parser state to be self-contained (no main-thread-side caches involved).

Why eventNotifier

The IO threads block on a custom condition (a list of pending clients + an eventNotifier). When the main thread hands off work, it triggerEventNotifiers, which writes a single byte to an eventfd or pipe. The IO thread's loop wakes from read on the eventfd and processes its queue. This is the same primitive the main thread uses to wake from epoll_wait when the IO thread completes work.

The eventfd-based design avoids the cost of a condition variable on Linux (futex_wait) and integrates naturally with the rest of the event loop.

Performance characteristics

Without IO threads, the bottleneck for very high RPS is usually read(2) + RESP parse + addReply* byte operations on the main thread. Each cycle has a fixed cost; the throughput ceiling is roughly 1 / cycle_time commands/sec.

With 4 IO threads, throughput on a benchmark like redis-benchmark -P 100 -t SET can approach 2× the single-thread number, because the parser and the writer are no longer serial with command dispatch.

For pipelined workloads (-P high), the win is largest. For single-shot workloads where each request is small and the network latency dominates, IO threads add overhead from the handoff dance and may reduce throughput.

The recommendation in redis.conf: turn IO threads on only when CPU profiling shows the main thread is bottlenecked on socket I/O.

Caveats

  • IO threads do not parallelise commands. Two clients submitting simultaneous mutating commands still serialise.
  • Commands marked client-no-evict yes and a few others still execute fully on the main thread.
  • MONITOR, MIGRATE, DEBUG SLEEP, and the lengthy admin commands are intentionally main-thread-only.
  • IO threads do not interact with replicas — replica I/O always runs on the main thread to keep replication-stream ordering simple.
  • In TLS mode, the OpenSSL state machine is per-connection; IO threads still work but the per-connection lock can serialise.

bio threads vs IO threads

These are different subsystems:

  • bio (src/bio.c) — three fixed worker threads that handle slow operations: closing AOF/RDB files, fsync'ing the AOF, freeing big objects. Job queues are simple.
  • iothread (src/iothread.c) — N worker threads that handle socket I/O on behalf of clients. Per-thread client lists with main-thread coordination.

Don't confuse them. bio is always on; iothread is opt-in.

Where to start modifying

  • Tune the handoff thresholdIO_THREAD_MAX_PENDING_CLIENTS and the heuristics inside sendPendingClientsToMainThreadIfNeeded in src/iothread.c.
  • Add a counter — bump a per-thread atomic and surface it under INFO server or INFO clients.
  • Diagnose contentionDEBUG IOTHREADS reports the per-thread queue lengths and offloaded counts.
  • Event loop — main-thread loop that the IO threads coordinate with.
  • Networking — what the IO threads actually do (parse and serialise).

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

IO threads – Redis wiki | Factory