Every so often a service that should respond in a millisecond takes exactly 40ms — or exactly 200ms — and profilers show nothing. The CPU is idle. The network is idle. Both TCP stacks are simply waiting for each other, and both are doing exactly what their RFCs recommend. This stall is the interaction of two algorithms that were each sensible on their own, shipped independently in the early 1980s, and have been ambushing request/response protocols ever since.
1984: the tinygram problem
John Nagle described the original problem in RFC 896 (January 1984). A Telnet session sends one keystroke at a time, and each keystroke becomes a 41-byte packet: one byte of data, 40 bytes of TCP/IP header. In the RFC's words, "This 4000% overhead is annoying but tolerable on lightly loaded networks." On congested networks it fed a feedback loop of drops and retransmissions that the same RFC named congestion collapse.
His fix is two lines of code and has no tuning parameters:
The solution is to inhibit the sending of new TCP segments when new outgoing data arrives from the user if any previously transmitted data on the connection remains unacknowledged. This inhibition is to be unconditional; no timers, tests for size of data received, or other conditions are required.
That's the entire Nagle algorithm. If you have a full-sized segment, send it. Otherwise, if anything is still in flight, buffer the small write until the ACK comes back. The elegance is that it's self-clocking: on a fast LAN the ACK returns in microseconds and coalescing barely happens; on a slow long-haul link, small writes pile up into one packet per round trip. No timer anywhere.
1989: the other good idea
Meanwhile, Berkeley's BSD stack added delayed acknowledgments, later standardized in RFC 1122 (October 1989):
A TCP SHOULD implement a delayed ACK, but an ACK should not be excessively delayed; in particular, the delay MUST be less than 0.5 seconds, and in a stream of full-sized segments there SHOULD be an ACK for at least every second segment.
The receiver holds the ACK for a while, hoping to piggyback it on data flowing the other way. For the workload Berkeley cared about — character-mode remote login, where the server echoes every keystroke — RFC 1122 notes it cuts the server's packet count by a factor of three: the ACK, the window update, and the echoed character ride in one segment.
Note what RFC 1122 did not do: it never mentions that this interacts with the algorithm it recommends 120 lines later. It says a TCP "SHOULD implement the Nagle Algorithm," adding only that "there MUST be a way for an application to disable the Nagle algorithm on an individual connection." That MUST is the socket option you know as TCP_NODELAY.
The collision: write-write-read
Each algorithm is a bet. Nagle bets an ACK will arrive soon, so buffering a small write is cheap. Delayed ACK bets response data will arrive soon, so holding the ACK is cheap. Put them on opposite ends of one connection and both bets lose simultaneously.
The classic trigger is a write-write-read pattern — say, a client that writes a request header, then writes the body, then reads the response:
- Client writes the header. Nothing is in flight, so it's sent immediately.
- Client writes the small body. The header is unacknowledged, so Nagle buffers the body.
- The server has the header but won't respond until it sees the body — so it has no data to piggyback an ACK on, and delayed ACK holds the ACK back.
- Deadlock, until the receiver's delayed-ACK timer fires. The ACK finally goes out, Nagle releases the body, and the exchange completes — tens or hundreds of milliseconds late.
A single write() followed by a read() never stalls: with nothing in flight, Nagle sends at once. The stall needs two-plus small writes back to back, which is exactly what buffered I/O layers, TLS record framing, and RPC libraries that write headers and payloads separately produce all the time.
The suspicious constants come from the receiver's timer. In Linux (include/net/tcp.h):
#define TCP_DELACK_MAX ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK */
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */HZ/25 is 40ms and HZ/5 is 200ms — which is why this bug wears those two numbers as a signature, and why RFC 1122's 500ms ceiling shows up on other stacks.
Nagle's own verdict
In a widely cited Hacker News comment, John Nagle himself assigned the blame:
That still irks me. The real problem is not tinygram prevention. It's ACK delays, and that stupid fixed timer. They both went into TCP around the same time, but independently. I did tinygram prevention (the Nagle algorithm) and Berkeley did delayed ACKs, both in the early 1980s. The combination of the two is awful.
His sharpest observation is about what delayed ACK is actually wagering: "A delayed ACK is a bet that the other end will reply to what you just sent almost immediately." For keystroke echo that bet wins. For nearly everything else — RPCs, database protocols, anything where the peer needs the complete request before answering — "the ACK delay mechanism loses the bet, over and over."
The knobs, and what each actually does
Per tcp(7), Linux gives you three relevant socket options:
TCP_NODELAYdisables Nagle: "segments are always sent as soon as possible, even if there is only a small amount of data." This fixes the stall from the sender's side and is the option everyone reaches for.TCP_CORKis the deliberate opposite: "don't send out partial frames" until the cork is cleared — useful for assembling a header plus a payload into one segment, with a 200ms ceiling on how long output stays corked. It can be combined withTCP_NODELAYsince Linux 2.5.71.TCP_QUICKACKattacks the receiver's half: ACKs are "sent immediately, rather than delayed." The trap is that it "is not permanent" — the kernel re-enters delayed-ACK mode on its own, so it must be re-armed around reads to keep working.
Nagle's advice in that same comment is to fix the receiver, not the sender: "Short version: set TCP_QUICKACK. If you find a case where that makes things worse, let me know." He also warns about the cost of the popular fix: with TCP_NODELAY, a loop of one-byte writes becomes one IP packet per write, and "this increases traffic by a factor of 40."
What to actually do in 2026
The industry has largely voted with TCP_NODELAY anyway, because you control your own sender but often not the peer's receiver. Go disables Nagle on every TCP connection unless you opt back in — newTCPConn in net/tcpsock.go calls setNoDelay(fd, true), and SetNoDelay's doc comment states plainly: "The default is true (no delay)."
Marc Brooker's "It's always TCP_NODELAY" makes the case for that default in modern distributed systems: RFC 896 solved a real 1984 problem, but today's services don't emit keystroke tinygrams — they batch at the application layer through serialization and buffered writers. Meanwhile in-datacenter round trips are measured in hundreds of microseconds, so a 40ms kernel-imposed stall is orders of magnitude worse than the header overhead it prevents.
The practical rules:
- If you see latency quantized at ~40ms, ~200ms, or ~500ms on small messages, suspect this interaction before anything else.
- Set
TCP_NODELAYon latency-sensitive connections — and then respect it by coalescing your own writes (onewritev()or a buffered flush per message, not per field). - If you control the receiving side of a request/response protocol,
TCP_QUICKACKaddresses the actual culprit — just remember it doesn't stick. - Keep Nagle only where it still earns its keep: chatty, uncoalesced small writes over links where header overhead genuinely matters.
Two correct local optimizations composed into a global stall, and the spec that recommended both never flagged the interaction. That's the durable lesson: algorithms that each hold a resource while betting on the other side's behavior deserve an integration test together, not just individual benchmarks.
References
- RFC 896: Congestion Control in IP/TCP Internetworks (John Nagle, 1984)
- RFC 1122: Requirements for Internet Hosts — Communication Layers
- John Nagle on the Nagle/delayed-ACK interaction (Hacker News)
- It's always TCP_NODELAY. Every damn time. — Marc Brooker
- tcp(7) — Linux manual page
- Linux v6.11 include/net/tcp.h (TCP_DELACK_MIN/MAX)
- Go net/tcpsock.go (TCP_NODELAY default)
