Worker threads
reactor pattern
concurrency
multithreading
asynchronous programming

What are worker threads, and what is their role in the reactor pattern?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Worker threads are background threads that perform tasks outside the main event-handling thread. In the Reactor pattern, their role is to keep the reactor responsive by handling work that would otherwise block the event loop.

What the Reactor Pattern Is Trying to Protect

The Reactor pattern is built around a small number of threads, often just one event loop, that wait for I/O readiness events and dispatch handlers quickly. The reactor is good at:

  • accepting connections
  • noticing readable or writable sockets
  • routing events to lightweight handlers

The reactor is bad at:

  • long CPU-heavy computation
  • blocking database calls
  • slow file operations
  • anything that keeps the event loop occupied for too long

If the reactor thread blocks, it stops reacting. That is the core problem worker threads solve.

What Worker Threads Do

A worker thread takes expensive or blocking work off the event loop so the reactor can return to polling for new events. The flow usually looks like this:

  1. the reactor receives an event
  2. the handler decides the work is too slow to do inline
  3. the task is submitted to a worker thread or thread pool
  4. the worker completes the task
  5. the result is sent back to the connection or next processing stage

This separation keeps the reactor focused on coordination rather than heavy execution.

A Small Python Example

The following example uses a ThreadPoolExecutor to represent worker threads. It is not a full production reactor, but it shows the division of responsibilities clearly.

python
1import selectors
2import socket
3from concurrent.futures import ThreadPoolExecutor
4
5selector = selectors.DefaultSelector()
6pool = ThreadPoolExecutor(max_workers=4)
7
8def slow_task(data):
9    total = sum(byte for byte in data)
10    return f"processed:{total}\n".encode()
11
12def on_accept(server_sock):
13    client, _ = server_sock.accept()
14    client.setblocking(False)
15    selector.register(client, selectors.EVENT_READ, on_read)
16
17def on_read(client_sock):
18    data = client_sock.recv(1024)
19    if not data:
20        selector.unregister(client_sock)
21        client_sock.close()
22        return
23
24    future = pool.submit(slow_task, data)
25    result = future.result()
26    client_sock.sendall(result)
27
28server = socket.socket()
29server.bind(("127.0.0.1", 9000))
30server.listen()
31server.setblocking(False)
32selector.register(server, selectors.EVENT_READ, on_accept)
33
34while True:
35    for key, _ in selector.select():
36        callback = key.data
37        callback(key.fileobj)

Even in this small example, the idea is visible: socket readiness is handled by the reactor-style loop, while slower processing is assigned to worker capacity.

Why a Thread Pool Is Common

Creating a new thread per request is usually too expensive. A worker thread pool avoids repeated thread creation and gives the system a controlled amount of concurrency.

That matters because an unbounded number of threads creates a new problem:

  • too much memory use
  • too much context switching
  • less predictable latency

So in practice, the reactor pattern often works with a fixed or bounded pool of workers.

CPU Work vs Blocking I/O

Worker threads are useful for both of these, but for different reasons:

  • for blocking I/O, they prevent the reactor from waiting
  • for CPU-heavy tasks, they prevent the reactor from burning its whole time slice on one request

The reactor thread should stay short-lived and mostly non-blocking. Once a handler turns into "real work," that work usually belongs somewhere else.

What Not to Offload

Not every event should go to a worker thread. Lightweight parsing, bookkeeping, and simple routing are often better done directly in the event loop. Offloading tiny tasks can add queueing overhead for no benefit.

A good rule is:

  • if it is quick and non-blocking, keep it in the reactor
  • if it can stall or run long, offload it

That balance is the difference between a clean design and a system that is asynchronous only on paper.

The Result Delivery Step

A full reactor design usually has a way to send worker results back without letting workers manipulate shared connection state unsafely. Some frameworks use callback queues, task schedulers, or another event posted back to the main loop.

That step matters because worker threads solve responsiveness problems, but they also introduce synchronization problems if shared state is handled carelessly.

Common Pitfalls

The most common mistake is doing blocking work directly in the event handler and then wondering why the "asynchronous" server stalls under load. If the reactor thread blocks, the architecture is not behaving like a reactor anymore.

Another issue is offloading everything to workers, including trivial tasks. That adds overhead and can make the system harder to reason about than a simple event loop.

Teams also forget that worker threads need a result handoff strategy. Offloading work without a clean way to return results just moves the complexity elsewhere.

Summary

  • Worker threads perform slower or blocking tasks outside the reactor event loop.
  • Their main job in the Reactor pattern is to keep the event loop responsive.
  • The reactor should focus on event detection and quick dispatch, not heavy execution.
  • Thread pools are usually better than spawning a new thread for every event.
  • Offload expensive work, but keep lightweight non-blocking work inside the reactor.

Course illustration
Course illustration

All Rights Reserved.