Your code wrote 8 bytes. The peer received them 40 ms later. Nobody configured a delay. This is the most famous silent latency bug in networking, and it's not one bug — it's two TCP mechanisms, each sensible in isolation, colliding on your connection. This article shows you the collision in packet traces and the exact switch that resolves it.
The two suspects
Nagle's algorithm (RFC 896, 1984). A sender-side rule designed to stop the "tinygram" problem: when a TCP connection is born for remote login, programs emit one byte at a time, and each byte was becoming its own 40-byte packet. Nagle's rule: if there is unacknowledged data in flight, don't send a new segment until the previous data is ACKed — unless the new data fills a full segment (MSS) or half the window. Small writes are therefore batched: write 8 bytes, send; write 8 more before the ACK returns, and they wait.
Delayed ACK (RFC 1122, 1989). A receiver-side rule: don't ACK every segment
immediately; hold the ACK up to 40 ms (200 ms on some stacks; Linux's default is
tcp_delack_max = 40 ms) hoping to piggyback it on the response data. For bulk
transfers, the receiver is usually writing data back anyway, so the ACK rides along
free — the delay is invisible.
Each rule saves a packet per interaction. Combined, they create a deadlock:
The 40 ms trap
sender receiver
|── [seg 1: 8 bytes] ────────>|
| (in flight, unacked) |
| Nagle: block new writes | Delayed ACK: wait up to 40ms
| until seg 1 is ACKed | hoping to piggyback an ACK
| |
|<── [ACK (after ~40 ms)] ────|
| Nagle unblocks |
|── [seg 2: 8 bytes] ────────>|Every write in the window waits roughly 40 ms — the delayed-ACK timer — because Nagle refuses to send while a segment is unacknowledged, and the receiver refuses to ACK promptly because it expects more data. The two timers never trigger each other; the packet just waits out the full ACK delay. This is why chat apps and request-response protocols feel "laggy" under Nagle+delayed ACK, while bulk downloads (where the window is full of segments and ACKs ride on data) are unaffected.
Seeing it, not just believing it
$ tcpdump -ni any tcp port 8080 -ttt
00:00:00.000000 10.0.0.5.44122 > 10.0.0.9.8080: Flags [P.], seq 1:9, 8 bytes
00:00:00.040095 10.0.0.9.8080 > 10.0.0.5.44122: Flags [.], ack 9
00:00:00.000071 10.0.0.5.44122 > 10.0.0.9.8080: Flags [P.], seq 9:17, 8 bytesThat 0.040095 gap between the first packet and its ACK is the smoking gun: the receiver
sat on the ACK for the full 40 ms. Note where the delay lives — the receiver's ACK
timer, not your sender. You can't fix it from your side by writing faster; the peer's
stack is doing the waiting.
The usual fix: TCP_NODELAY
Nagle is a socket option on the sender: TCP_NODELAY disables it outright. It's set by
default in Node.js, Go (net.TCPConn with SetNoDelay(true)), Redis, and most modern
servers — precisely because they knew about this trap:
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));$ sysctl net.ipv4.tcp_slow_start_after_idle # related: Linux waits ~1s after idle
net.ipv4.tcp_slow_start_after_idle = 1When Nagle is actually right
Nagle is not broken for everything. For bulk, streaming workloads — video, file transfer, log shipping — Nagle reduces packet count per byte by coalescing small writes into full segments, and delayed ACK's piggybacking saves a packet per transaction. The interaction only bites when the traffic is request-response with small payloads: each write is under MSS, the response is the only piggyback opportunity, and the ACK gets parked. The decision rule:
- Request/response with small messages (RPC, chat, HTTP/2 frames): TCP_NODELAY on.
- Bulk one-way streaming where the peer writes nothing back: Nagle on (or better, batch writes to full segments yourself — Nagle exists to hide your laziness).
The deeper fix: stop writing small
Nagle's real lesson is that the kernel's coalescing is a proxy for your buffering.
The clean architecture is: your protocol reads a whole message, then writes once — with
TCP_CORK on Linux you can even tell the kernel "hold everything until I uncork", and
it emits one full segment. Combined with TCP_NODELAY for interactivity, that's the
idiomatic pattern: coalesce at the application boundary, never at the timer boundary.
The mental model
- Nagle batches the sender's small writes; delayed ACK batches the receiver's acknowledgements. Each is correct in isolation; together they deadlock small messages.
- The 40 ms isn't a network problem — it's the peer's ACK timer, and
tcpdumpshows it exactly once per interaction. TCP_NODELAYon request-response connections is not a micro-optimization; it's the difference between "feels local" and "feels remote".- If you ever see a steady ~40 ms (or ~200 ms) added to every small request and your
stack isn't setting
TCP_NODELAY, you've found the bug without touching the network.