← writing

2.8M requests/sec on one core: what a share-nothing reactor buys you

Notes from writing an HTTP/1.1 server in C++20 against raw Linux syscalls — epoll, SO_REUSEPORT, sendfile — and a parser that never allocates.

14 July 2026·1 min readc++20linuxnetworkingperformance

The server has no framework underneath it. It talks to Linux directly: epoll in edge-triggered mode, SO_REUSEPORT, sendfile(2), accept4, eventfd. The interesting part is not any one syscall, it is the shape they let you build.

One reactor per thread, nothing shared

Every worker thread owns its own listen socket, its own epoll instance and its own timer wheel. The kernel's SO_REUSEPORT group is the only coordination point: it distributes incoming connections across the listeners, so the workers never have to hand connections to each other.

The consequence is that there are zero cross-thread locks on the hot path. No shared accept queue, no shared connection table, no work stealing. A connection lives and dies on the thread that accepted it.

A parser that doesn't allocate

The RFC 7230 parser is a state machine over the receive buffer. It advances a cursor, records offsets for the method, target and header fields, and hands back views into the buffer. The only allocation it can ever make is growing the receive buffer itself when a request doesn't fit — and on a benchmark load, that happens once per connection and never again.

That is where the headline number comes from: about 2.8 million requests parsed per second on a single core, roughly 294 MB/s, measured on a WSL2 laptop. It is not a number about the network stack. It is a number about how little work sits between read() and the handler.

Shutdown and idle connections

Two things that tutorials skip and production servers cannot:

  • Graceful shutdown. Each reactor has an eventfd. Writing to it wakes the epoll_wait, the reactor drains its connections and exits. No signals crossing threads.
  • Idle timeouts. Each worker runs a timer wheel and evicts connections that have gone quiet, so a client that opens a socket and walks away cannot pin a file descriptor forever.

Thirty tests cover the parser edge cases, the connection lifecycle and the shutdown path.


Questions or corrections? Email me or find me on GitHub.