Hands-on TCP_QUICKACK lab: default QA=1 on loopback; WWR set-once ~18 µs; reassert QA=1 under Nagle p95 ~48 ms, OutSegs ~13×. Real numbers, no Docker.
7 min read
TCP_QUICKACK is the ACK-side twin of the Nagle / TCP_NODELAY story. Folklore says “turn on quickack to kill delayed ACK.” On Linux the option is best-effort, often not sticky, and on this loopback the default already reported 1.
This is a hands-on lab with measured numbers:
getsockopt(TCP_QUICKACK) returns after connect and after one echo.TCP_NODELAY × default / set-once quickack.OutSegs deltas when you thrash quickack under Nagle.TCP_NODELAY lab (sender coalesce ≠ ACK policy).Related links:
Lab honesty (1 Oct 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5 TCP echo on 127.0.0.1. No Docker. No public bind. Affiliates: 0. This is a loopback microbench, not a WAN delayed-ACK capture.
Verdict up front: default QUICKACK after connect was 1. WWR under Nagle with set-once qa0/qa1 stayed ~18–20 µs p50. Reasserting QUICKACK=1 after every send under Nagle blew mean to ~12 ms (mean/p50 ~326×, p95 ~48 ms) and ~13× more OutSegs on multi-write.
Delayed ACK lets a receiver wait briefly before acknowledging a single small segment, hoping to piggyback on a reverse data segment. TCP_QUICKACK asks the stack for quick acknowledgements. On Linux it is documented as a mode that the stack may clear — not a permanent “disable delayed ACK forever” switch.
Related links:
| Knob | Layer | What we measured here |
|---|---|---|
TCP_NODELAY | sender coalesce (Nagle) | separate lab + reuse matrix here |
TCP_QUICKACK | receiver ACK timing | set-once vs reassert on loopback |
App buffering / TCP_CORK | deliberate batch | not the focus of this post |
Probe: after create_connection, getsockopt(TCP_QUICKACK) returned 1. Set to 0 → 0. Set to 1 → 1. After one echo it still read 1.
n=2000 timed RTTs (after warmup):
| Arm | p50 | p95 | mean | RPS |
|---|---|---|---|---|
| Nagle + default QA | 10.3 µs | 17.7 | 14.1 | ~71k |
| Nagle + qa0 once | 11.1 | 22.9 | 15.1 | ~66k |
| Nagle + qa1 once | 14.2 | 16.3 | 16.4 | ~61k |
| NODELAY + default QA | 10.7 µs | 16.8 | 12.5 | ~80k |
| NODELAY + qa0 once | 11.1 | 25.0 | 16.4 | ~61k |
| NODELAY + qa1 once | 10.7 | 19.1 | 12.9 | ~78k |
On this chatty reuse shape, NODELAY still mattered more than set-once quickack. Default QA already looked “on.”
Two 32 B writes, then one read. Classic place Nagle waits on an ACK.
| Arm | p50 | p95 | mean | mean/p50 | RPS |
|---|---|---|---|---|---|
| Nagle + default QA | 29.9 µs | 37.5 | 40.3 | 1.35 | ~25k |
| Nagle + qa0 once | 20.1 µs | 29.4 | 21.2 | 1.05 | ~47k |
| Nagle + qa1 once | 17.7 µs | 19.7 | 18.8 | 1.06 | ~53k |
| Nagle + qa0 reassert | 19.8 | 33.9 | 25.3 | 1.28 | ~39k |
| Nagle + qa1 reassert | 36.5 | ~48 ms | ~11.9 ms | ~326 | ~84 |
| NODELAY + qa0 once | 16.5 | 22.3 | 20.6 | 1.24 | ~49k |
| NODELAY + qa1 once | 26.6 | 49.9 | 39.0 | 1.46 | ~26k |
Set-once qa0 vs qa1: both fine; no classic ~40 ms stall on this loopback. The failure mode we actually hit was reasserting QUICKACK=1 after every send under Nagle — p95 jumped into the tens of milliseconds and mean lied hard.
Stability×3 for WWR Nagle qa0 once p50: 18.9 / 31.3 / 17.6 µs (median 18.9).
Related links:
16 writes of 8 B, then drain. n=500.
| Arm | p50 | mean | OutSegs Δ | RPS |
|---|---|---|---|---|
| Nagle + default QA | 46 µs | 58 | 3886 | ~17k |
| Nagle + qa0 once | 50 | 60 | 2896 | ~17k |
| Nagle + qa1 once | 50 | 64 | 2790 | ~16k |
| Nagle + qa1 reassert | ~44 ms | ~37 ms | 36199 (~13×) | ~27 |
| NODELAY + qa0 once | 129 µs | 142 | 11711 | ~7k |
Reassert-qa1 under Nagle did not “fix latency”; it fragmented the session (far more segments) and collapsed RPS. NODELAY still emitted more segments than Nagle set-once — same qualitative story as the NODELAY lab.
setsockopt(QUICKACK,1) in a hot send loop — measured disaster under Nagle.getsockopt(TCP_QUICKACK) after connect before claiming defaults.send.Related links:
On this loopback box, TCP_QUICKACK defaults to 1 after connect. WWR under Nagle with set-once qa0/qa1 stayed ~18–20 µs p50. Reasserting QUICKACK=1 after every send under Nagle produced mean ~12 ms, p95 ~48 ms, and ~13× OutSegs on multi-write. Treat quickack as a sharp, best-effort tool — not a hot-path toggle.
Evidence path on the lab box: lab-evidence/30-tcp-quickack/results/. Affiliates: 0.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5 TCP echo 127.0.0.1. getsockopt default QUICKACK=1 after connect. Reuse 1 B: Nagle default ~71k RPS vs NODELAY default ~80k. WWR Nagle set-once qa0 p50 20.1 µs / qa1 17.7 µs (no classic 40 ms). WWR Nagle qa1 reassert mean 11.9 ms mean/p50 326× p95 ~48 ms RPS ~84. Multi 16×8 B Nagle qa1 reassert OutSegsΔ 36199 vs ~2.8–3.9k set-once; p50 ~44 ms. Stability WWR qa0 p50 median 18.9 µs. No Docker. Affiliates: 0. Evidence: lab-evidence/30-tcp-quickack/.
Hands-on TCP_NODELAY vs Nagle: 1-byte reuse 124k vs 73k RPS; 32×16B writes favor Nagle (~1.7× fewer OutSegs). Delayed-ACK mean trap on localhost.
Lab30 Sept 20266 min read
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20264 min read
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
Summary1 Oct 20263 min read