Build1 publisher3 min readPublished
HAProxy's 206ms token lag comes from an MSG_MORE tag on every in-flight body write
An HAProxy employee traced the 206ms first-token delay out of the proxy and into the kernel send path, where the H1 mux marks each body write as incomplete and the system holds it back until it can fill a packet.
The Engineer · Build desk

What happened
- A benchmark put nginx, Caddy, Traefik and HAProxy in front of a scripted LLM emitter sending one tiny frame every 50 milliseconds; through the first three, tokens arrived 2 to 3 milliseconds later, one per read.
- Through HAProxy the first token showed up 206 milliseconds late, and the tokens after it arrived in bursts of five with no gap between them.
- The post concluded that HAProxy buffers tokens; an HAProxy employee says nothing in the proxy collects them, because HAProxy forwards the body as it arrives and the wait happens in the kernel's send path.
- In the 3.4 source he tested, the H1 mux sets the kernel's MSG_MORE tag on every in-flight body write, via CO_SFL_MSG_MORE in src/mux_h1.c, and clears it when the body ends.
- The benchmark author reports that the same proxy carrying 1.1 KB frames instead of 60-byte ones delivers the first token at 53 milliseconds with near-zero coalescing.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision A team reading that table as a proxy shortlist would fund a migration to change a default. Change the upstream to emit kilobyte frames and the same table returns a different verdict.
- cost Clearing the flag for streaming makes the workloads HAProxy is usually bought for more expensive: at high request rates, every un-batched small write is an extra packet and an extra syscall.
- constraint Tunnels, WebSockets and CONNECT are already exempt, so an override only has to reach the routes that stream an HTTP response body, not the whole frontend.
- contradiction Both accounts accept the same 206 milliseconds and disagree about where the wait happens. Only one of the two readings lets you keep the proxy you have.
MSG_MORE is a flag on the send call. Nothing inside the proxy is collecting tokens; the flag tells the kernel that more data is coming and to hold what it was just handed [8]. HAProxy already sets TCP_NODELAY on its TCP sockets, so Nagle's algorithm is not the thing holding the token [7]. A response body that arrives in one piece clears the flag straight away. A token stream never does: 60 bytes every 50 milliseconds, with a round trip between each write, so the corked writes sit in the kernel until the tag clears or a timer cycle releases them [18].
The configuration manual describes the effect in the same units the benchmark measured. The system "waits for enough data to be available in order to only send full packets. Typical delays are around 200 ms per round trip" [10]. The measured 206 milliseconds is about four of the emitter's 50 millisecond intervals [23].
For that figure to transfer to your deployment, your upstream has to emit frames that are small relative to a packet and pause between them. The benchmark author says frames-per-read is a property of the stream shape; it moves when the stream does [14]. His 1.1 KB frames are roughly eighteen times the 60-byte payload [22], and the delay they produce is about a quarter of the 206 milliseconds [21].
Clearing the flag moves the cost onto the traffic HAProxy is usually bought for. The troubleshooting guide says Nagle "definitely remains enabled when forwarding an HTTP body (and this contributes to the performance improvement there by reducing the number of packets)" [16]. The reason is header overhead, described in RFC 896 in 1984: a sender holding a small amount of data waits for a full packet's worth, or for the receiver to acknowledge the previous packet [11]. A 60-byte write carries header overhead, and those headers can double the bytes transmitted [12]. For a download or a bulk API response, 40 milliseconds of accumulated batching once per connection is invisible to whoever is waiting on it [19].
The default was tuned around a specific idea of interactive traffic. Batching is skipped in pure TCP mode and in tunnels, and WebSockets and CONNECT requests are documented as unaffected [17]; the author says the picture at the time was a tunnel or an upgraded socket [26]. That list left out an HTTP response body emitted one token at a time.
The account comes from inside the vendor. "NOTE: I work at HAProxy, so I'm probably a bit biased," the author wrote, and said the post is his own opinion and research, not official from the company [5][24]. He says the behaviour is reversible with one line of config. The post does not name the directive [25].
What to watch
- An independent rerun of the four-proxy table on a stock HAProxy 3.4 build, by someone not employed by HAProxy.
- Whether HAProxy documents or ships a directive that clears MSG_MORE for streamed response bodies.
- Whether the benchmark author republishes the table with the 1.1 KB frame result beside the 60-byte one.