Plate 60
SO_REUSEPORT vs Single Listen: Independent Queues and ListenOverflows
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.
Aditya Challa6 min read
Intro — what this post promises
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:
- What
ssshows: one shared listen queue vs N independent queues. - A fair echo microbench: single-listen workers vs
SO_REUSEPORTworkers. - Accept affinity (which worker PID answers).
- A slow-accept flood where backlog pressure creates
ListenOverflows. - What this does not claim (WAN, magic RPS multipliers, older-kernel thundering-herd lore alone).
Related links:
- HTTP Keep-Alive vs Connection: close lab
- Nginx limit_req rate-limit lab
- Why your average latency graph is lying (p50 / p95 / p99)
- How to read server monitoring graphs
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).
What SO_REUSEPORT changes
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.
Lab topology
Arms:
- Fast 64-byte echo, n=8000, concurrency=64.
- Affinity probe: server replies with
os.getpid(), n=4000. - Slow accept flood:
sleep(delay)after accept, small backlog, concurrent connect flood; deltas from/proc/net/netstatListenOverflows/ListenDrops.
ss: the picture worth saving
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.
Fast echo: RPS basically tied
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.
Affinity: both modes spread work
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.
Slow-accept flood: where reuseport showed up
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.
Pitfalls we hit (or avoided)
- Claiming an RPS win from a connect-heavy echo — we measured a tie and kept it.
- Forgetting to look at
ss— the multi-LISTEN fingerprint is the config proof. - Ignoring overflow counters — success % alone understates how hard the kernel worked (
ListenOverflows). - Treating reuseport as a WAN feature — it is same-host / same-VIP accept steering.
- Skipping percentiles — flood survivors had ugly p95; see evidence JSON.
Related links:
Practical checklist
- Multi-worker proxy/app: confirm
reuseport(or equivalent) if you want per-worker listen queues. - Verify with
ss -ltnp— expect N LISTEN lines on the port, not one. - Under incidents, check
ListenOverflows/ListenDropsalongside CPU. - Size backlog and accept/worker capacity; reuseport shards the queue, it does not create accept CPUs.
- Benchmark your handler; do not cite our echo RPS as a universal multiplier.
Verdict
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
What I found running this
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/.
Related links
Plate 95
TCP Listen Backlog Lab: somaxconn, ListenOverflows, and Silent Drops
Hands-on TCP backlog lab: backlog=1 + slow accept → 34/200 OK, ListenOverflows +357; backlog=128 stays 200/200. Real ss + netstat numbers.
Observability & SRE · 30 Sept 2026
Plate 12
islice vs list Slice Windows: Localhost Lab
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.
Observability & SRE · 1 Oct 2026
Plate 07
heapq.merge vs sorted(chain): Localhost Lab
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.
Observability & SRE · 1 Oct 2026