Hands-on SO_REUSEPORT lab: ss shows 4 listen queues; backlog=8 flood 72% vs 62% OK; ~41% fewer ListenOverflows. Real localhost numbers, no Docker.
6 min read
SO_REUSEPORT lets multiple processes each bind() + listen() on the same IP:port. Nginx, Envoy, and many multi-worker servers expose it as reuseport. The folklore says it “scales accept.” This lab measures what actually moved on one box.
This is a localhost lab with measured numbers:
ss shows: one shared listen queue vs N independent queues.SO_REUSEPORT workers.ListenOverflows.Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5 multi-process TCP servers bound to 127.0.0.1 only. Modes: inherited single listen() FD vs per-worker SO_REUSEPORT. No Docker. No public bind. Affiliates: 0.
Verdict up front: on this kernel, echo RPS was tied (~9.8k) and accept affinity was already fair without reuseport. The clear, measurable win was independent listen queues in ss and fewer ListenOverflows under a tight backlog + slow accept (e.g. +476 → +394 overflows; 62% → 72% client OK on the backlog=8 flood).
Without it, N workers typically share one listening socket (inherited FD). With it, each worker owns a listening socket on the same tuple; the kernel steers new SYNs across those sockets.
Related links:
Mental model that matched our lab:
| Choice | What it did here |
|---|---|
| Single listen FD | One ss LISTEN line; one Send-Q (= backlog) shared by all workers |
SO_REUSEPORT | Four LISTEN lines on the same port; backlog per socket |
| Fast echo + connect-per client | Bottleneck was client/connect — RPS tied |
| Slow accept + small backlog | Overflow counters moved; reuseport dropped fewer |
Pair this with the listen-backlog lab: reuseport does not remove the need for accept capacity; it shards the shock absorber.
Arms:
os.getpid(), n=4000.sleep(delay) after accept, small backlog, concurrent connect flood; deltas from /proc/net/netstat ListenOverflows / ListenDrops.With backlog=2 and 4 workers:
| Mode | ss -ltn shape |
|---|---|
| single | 1 LISTEN line, Send-Q=2 |
| reuseport | 4 LISTEN lines, Send-Q=2 each |
That is the operational tell. If you expected “four queues” but ss shows one line, you are on shared-listen, not reuseport.
n=8000, concurrency 64, payload 64 B, 4 workers:
| Mode | Approx RPS | p50 | p99 |
|---|---|---|---|
| single | ~9824 | 5.37 ms | 16.7 ms |
| reuseport | ~9842 | 5.25 ms | 21.2 ms |
No meaningful win. The client was opening a fresh TCP connection per op against a cheap handler — we were not backlog-bound. Honest labs say so.
n=4000 probes, reply = worker PID:
| Mode | Unique workers | Share range |
|---|---|---|
| single | 4 | ~23.3%–26.5% |
| reuseport | 4 | ~24.2%–25.9% |
On this 6.12 kernel, shared accept() was already balanced. Do not sell reuseport as the only path to “fair workers” without measuring your kernel and workload.
Same flood shape, 4 workers, intentional accept delay so the listen queue matters.
backlog=8, delay=25 ms, n=400 connects (200 client threads):
| Mode | Client OK | OK % | ListenOverflows Δ | ListenDrops Δ |
|---|---|---|---|---|
| single | 249 / 400 | 62.2% | +476 | +476 |
| reuseport | 288 / 400 | 72.0% | +394 | +394 |
backlog=2, delay=40 ms, n=300:
| Mode | Client OK | OK % | LO / LD Δ |
|---|---|---|---|
| single | 163 / 300 | 54.3% | +774 |
| reuseport | 170 / 300 | 56.7% | +456 (~41% fewer overflows) |
Healthy control — backlog 128, delay 0, n=300: both modes 300/300, overflow delta 0.
Reading: reuseport’s extra queues absorbed more of the same shock before the kernel dropped. Client OK % still tracks accept capacity (the sleep). Raising backlog or speeding accept remains mandatory — reuseport is not a substitute.
ss — the multi-LISTEN fingerprint is the config proof.ListenOverflows).Related links:
reuseport (or equivalent) if you want per-worker listen queues.ss -ltnp — expect N LISTEN lines on the port, not one.ListenOverflows / ListenDrops alongside CPU.SO_REUSEPORT on this box bought independent listen queues and fewer ListenOverflows under tight backlog + slow accept (72% vs 62% OK on the backlog=8 flood; ~41% fewer overflows on the backlog=2 arm). It did not multiply tiny-echo RPS, and accept affinity was already fair. Use it when you want sharded accept queues; still fix accept capacity and backlog.
Evidence path on the lab box: lab-evidence/16-so-reuseport/results/. Affiliates: 0.
Lab evidence
Lab 30 Sep 2026 IST. Python 3.13.5, 4 workers, 127.0.0.1 only. ss: single=1 LISTEN Send-Q; reuseport=4 LISTEN lines. Echo n=8000 c=64: single ~9824 rps vs reuseport ~9842 rps (tied). Affinity n=4000: both ~fair 23–26% shares. Flood backlog=8 delay=25ms n=400: single 249/400 (62.2%) LO+476; reuseport 288/400 (72.0%) LO+394. Flood backlog=2 delay=40ms n=300: LO+774 vs LO+456 (~41% fewer). Healthy backlog=128: both 300/300 LO+0. Affiliates: 0. Evidence: lab-evidence/16-so-reuseport/.
Hands-on TCP backlog lab: backlog=1 + slow accept → 34/200 OK, ListenOverflows +357; backlog=128 stays 200/200. Real ss + netstat numbers.
Lab30 Sept 20266 min read
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Lab1 Oct 20264 min read
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Lab1 Oct 20264 min read