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.
6 min read
Nagle's algorithm makes TCP hold back a small segment while an earlier small segment is still unacknowledged, so tiny writes get merged into fewer packets, and setting TCP_NODELAY turns it off. In this localhost lab, TCP_NODELAY won for chatty 1-byte request/response (about 124k vs 73k RPS) but lost when the app made 32 tiny writes before reading (about 1.7x more segments and half the RPS), so measure your own write pattern before you flip it.
This is a localhost lab with measured numbers:
getsockopt(TCP_NODELAY) reports on live sockets.TCP_QUICKACK=0: mean ≫ p50 when Nagle meets delayed ACK.TCP_CORK control that looks like deliberate coalesce.Related links:
Lab honesty (30 Sep 2026 IST): Shared Linux lab box (8 cores, kernel 6.12). Python 3.13.5 TCP echo on 127.0.0.1:18310 (Nagle) and :18311 (TCP_NODELAY on accepted sockets). No Docker. No public bind. Affiliates: 0.
Verdict up front: reuse 1-byte echo favored NODELAY (~124k vs 73k~~ RPS). Batched 32×16 B writes favored Nagle (~~1.7× fewer OutSegs, ~2× higher RPS). With TCP_QUICKACK=0, Nagle write-write-read mean 119 µs vs p50 23 µs — the delayed-ACK tail — while NODELAY stayed at mean 22 µs.
Nagle delays sending a small segment while an unacknowledged small segment is in flight, hoping to coalesce. TCP_NODELAY turns that off so each send can become a segment sooner.
Related links:
| Pattern | What we saw on loopback |
|---|---|
| One small request, one response (reuse) | NODELAY lower latency / higher RPS |
| Many tiny writes before a read | Nagle fewer segments, higher RPS |
| Two writes then read + delayed ACK pressure | Nagle mean blown by tail; NODELAY stable |
| Connect-per request | Handshake noise dominates; knob hard to see |
getsockopt check: NODELAY arm returned 1, Nagle arm returned 0.
Single TCP connection, send payload, read echo. n=3000, warmup=150.
| Size | Arm | mean | p50 | p95 | RPS |
|---|---|---|---|---|---|
| 1 B | Nagle | 13.6 µs | 11.9 | 19.8 | ~73.4k |
| 1 B | NODELAY | 8.1 µs | 7.6 | 10.2 | ~124.1k |
| 64 B | Nagle | 10.6 µs | 11.3 | 12.8 | ~94.7k |
| 64 B | NODELAY | 9.8 µs | 9.2 | 10.8 | ~102.2k |
For chatty reuse protocols (game ticks, fine-grained RPC), NODELAY’s ~1.7× RPS on 1-byte messages is the intended win.
32 writes of 16 B (512 B total), then drain the echo.
| Arm | mean | RPS | OutSegs Δ (800 rounds) |
|---|---|---|---|
| Nagle | 41 µs | ~24.4k | 14202 |
| NODELAY | 79 µs | ~12.6k | 24296 (~1.71×) |
| TCP_CORK then uncork (control) | 38 µs | ~26.2k | (not counted) |
NODELAY was slower here because it emitted more segments. Nagle (and cork) batched. If your app already buffers, NODELAY is fine; if it write()s tiny pieces, Nagle or application-level buffering / cork saves work.
Two writes totaling 64 B, then one read. Client set TCP_QUICKACK=0 to stress delayed ACK (best-effort on this stack).
| Arm | mean | p50 | p95 | RPS |
|---|---|---|---|---|
| Nagle | 118.8 µs | 23.4 | 44.6 | ~8.4k |
| NODELAY | 22.3 µs | 17.9 | 27.5 | ~44.9k |
This is why averages lie: Nagle’s p50 still looked fine while mean blew up. Pair with the percentiles playbook before you “fix” latency with one knob.
Related links:
Honesty: under default quickack on this loopback we did not see a classic ~40 ms stall. The trap still showed as a mean/p50 gap once quickack was forced off.
Keep-alive removes connect cost (our connect-per arms were ~20–27 µs of handshake noise). UDS removes TCP entirely for same-host IPC. TCP_NODELAY only changes TCP sender coalesce policy. Three different layers; three different graphs.
Related links:
sends or one buffered write.TCP_NODELAY and measure p50/p95, not only mean.TCP_CORK over blind NODELAY.getsockopt and, if needed, segment counters (/proc/net/snmp OutSegs).TCP_NODELAY is not a free latency cheat code. On this box it won chatty 1-byte reuse (~124k vs 73k~~ RPS) and lost batched tiny writes (~~1.7× more segments, ~half the RPS). Under delayed-ACK pressure, Nagle’s mean lied while p50 looked calm. Measure the write pattern you actually ship.
Evidence path on the lab box: lab-evidence/18-tcp-nodelay/results/. Affiliates: 0.
A TCP sender rule from RFC 896: while a small segment is unacknowledged, further small data waits so it can be coalesced into one segment. It cuts packet count at the cost of some latency.
No. It helps request/response protocols that send one small message and wait for a reply. If your app writes many tiny pieces, Nagle (or app buffering / TCP_CORK) sends fewer segments and was about 2x faster here.
It interacts with delayed ACKs. With TCP_QUICKACK off, Nagle's write-write-read mean was 118.8 µs while p50 stayed at 23.4 µs, a tail that the average exposes. TCP_NODELAY held a 22.3 µs mean.
Lab evidence
Lab 30 Sep 2026 IST. Python 3.13.5 echo 127.0.0.1:18310 (Nagle) / :18311 (NODELAY). getsockopt on=1 off=0. Reuse 1B: Nagle mean 13.6µs ~73.4k RPS; NODELAY 8.1µs ~124.1k. Multi 32×16B segs arm: Nagle 41µs ~24.4k OutSegsΔ=14202; NODELAY 79µs ~12.6k OutSegsΔ=24296. WWR+QUICKACK=0: Nagle mean 118.8µs p50 23.4; NODELAY mean 22.3µs p50 17.9. Cork control ~38µs. No classic 40ms stall on default loopback quickack. Affiliates: 0. Evidence: lab-evidence/18-tcp-nodelay/.
Explore the product
Untested by us. Check what's included, the license and refund terms before you buy.
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.
Summary1 Oct 20267 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