2026-07-19
Before Linux 3.9, if you wanted N threads accepting connections on port 80, you had two bad choices: one thread calling accept() in a loop (single-threaded bottleneck), or N threads all calling accept() on the same shared listening socket (thundering herd, plus a contended kernel spinlock on the accept queue). The classic SO_REUSEADDR only relaxed time-based sharing — it let you rebind after TIME_WAIT. It did not let two live sockets bind to the same port.
SO_REUSEPORT changes the rule. Set it before bind(), and multiple sockets — from the same process or different processes — may all bind to the same (protocol, address, port) tuple. Each socket gets its own accept queue. The kernel then hashes each incoming SYN's 4-tuple (src IP, src port, dst IP, dst port) and steers the connection to exactly one of the listening sockets. Load balancing is done in the kernel, before the packet ever reaches user space.
The hash is deterministic per flow, so all packets of one TCP connection land on the same accept queue — no reshuffling mid-handshake. On UDP it's per-datagram source-hashed, so a client's packets stick to one socket.
Concrete example — nginx worker model: Modern nginx (with reuseport on the listen directive) spawns N worker processes, each of which independently opens a socket, sets SO_REUSEPORT, and binds to :80. Before this option, one master would accept() and hand FDs to workers, or all workers would share one socket and fight over the accept mutex. With SO_REUSEPORT, each worker has a private accept queue that only its own kernel thread drains. Cloudflare measured roughly a 3x throughput improvement and ~30% latency-tail reduction on high-connection-rate workloads after switching.
Rule of thumb: If your listener's accept rate exceeds ~50k connections/sec on a single core, or if perf shows time in inet_csk_accept / spinlock contention, switch to SO_REUSEPORT with one socket per worker thread pinned to one CPU. Cost is near-zero; benefit scales linearly with cores until NIC RSS becomes the bottleneck.
The sharp edge: when a worker dies with connections queued on its accept queue, those SYN-ACKed but not-yet-accepted connections are silently dropped — the kernel doesn't redistribute them. Under graceful shutdown you must stop binding new sockets first, then drain. Also: since Linux 4.5, SO_ATTACH_REUSEPORT_EBPF lets you replace the built-in 4-tuple hash with your own eBPF program, e.g. to steer by cookie or route by tenant.
