Repository navigation
fix(server): set TCP_NODELAY on accepted connections - #218
Merged
Merged
Conversation
With Nagle enabled, the small frame that ends an HTTP/2 response (trailers or END_STREAM) can be held until the client's delayed ACK, adding ~40ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; no actionable issues were found. SummaryAdds
Reviews (1) · Last reviewed commit: "fix(server): set TCP_NODELAY on accepted..." · Reviewed by Greptile |
Merged
shikhar
added a commit
that referenced
this pull request
Nov 13, 2026
Uses features from axum-server 0.9 (#220) and simplifies the server setup. Only `src/bin/server.rs` changes. ## Accept errors axum-server drops connection failures silently unless an `accept_error_handler` is set. The handler now logs them: - **Listener failures**, such as running out of file descriptors, log at `warn`. axum-server waits 50 ms between retries, so this is at most about 20 lines a second. - **Connection setup failures**, mainly failed or timed-out TLS handshakes, log at `debug` with the client address. Load-balancer health checks and scanners make these routine. The server's subscriber is built without `env-filter` and only shows `info` and above, so these stay hidden as before. ## TCP keepalive `TcpConfigAcceptor` replaces `NoDelayAcceptor`. It keeps `TCP_NODELAY` from #218 and adds keepalive: probes start after 60s idle, repeat every 10s, and give up after 3 failures. Without it, the server has no idle timeout on HTTP/1.1 or HTTP/2. A client that disappears without closing its connection, because its host died or a NAT or load balancer dropped the flow, leaves it open indefinitely. The connection holds a task and a file descriptor and counts in `cachey_connections`. Long-lived HTTP/2 connections make this more likely. ## Server setup The three TLS modes each repeated the bind, acceptor, handle and serve steps. Now the TLS match only produces an `Option<RustlsConfig>` and the server is configured once. The TLS config also loads before the cache is built, so a bad certificate path fails before cache startup. The startup log now reports the certificate source and the listen address on separate lines. ## Testing Clippy with warnings denied, rustfmt, and `cargo nextest run --locked --all-features` (206 passed, 1 skipped) pass. The integration tests serve through `axum::serve`, so I also ran the binary locally on macOS: - **TLS (`--tls-self`):** HTTPS requests succeed over HTTP/2. Plain HTTP to the TLS port and a bare connect-and-close log nothing at the default level. - **With `ulimit -n 64`:** opening 80 connections logged `failed to accept connection` warnings (`Too many open files`). The server kept serving afterwards. - **Socket options:** `lsof` shows `SO=KEEPALIVE=60000` on an accepted socket, and the TCP flags include `TF_NODELAY`. - **Shutdown:** both TLS and plain HTTP modes exit cleanly on SIGTERM. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
shikhar
pushed a commit
that referenced
this pull request
Nov 13, 2026
## 🤖 New release * `cachey`: 0.13.4 -> 0.13.5 <details><summary><i><b>Changelog</b></i></summary><p> <blockquote> ## [0.13.5](0.13.4...0.13.5) - 2026-11-13 ### Added - *(server)* log accept errors and enable TCP keepalive ([#221](#221)) ### Fixed - *(server)* set TCP_NODELAY on accepted connections ([#218](#218)) ### Other - *(deps)* upgrade dependencies ([#220](#220)) </blockquote> </p></details> --- This PR was generated with [release-plz](https://github.com/release-plz/release-plz/). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The server never sets
TCP_NODELAY.axum-server's default acceptor leaves Nagle's algorithm on, and turning it off is opt-in viaNoDelayAcceptor.Under HTTP/2, a fetch response ends with a small separate frame: the trailers frame from the body stream, or an empty
END_STREAMdata frame. That frame often goes out while the previous small segment is still unacknowledged, so Nagle holds it until the ACK arrives. A Linux client delays that ACK by up to 40 ms, so the client finishes reading the response about 40 ms late.HTTP/1.1 clients don't hit this. With
Content-Lengththe response ends at its last byte, and no trailing frame is sent.We saw this after moving a client from HTTP/1.1 to HTTP/2 with prior knowledge. Client-observed fetches in the 40–80 ms range became about 7× more common. The server's own first-chunk and download latencies didn't change.
Repro
A minimal Linux loopback test mirrors the fetch response: a 206 with
Content-Length, the body streamed throughStreamBody, and a trailing trailers frame. The client isreqwestwithhttp2_prior_knowledge(), or HTTP/1.1 for comparison. The server isaxum-server0.8 with rustls, with and withoutNoDelayAcceptor. Responses are 2 KiB, 2000 requests per case:TCP_NODELAYOn a real network the stall happens less often than on loopback, because other traffic on the connection usually carries the ACK sooner. The fix is the same either way.
What
Use
NoDelayAcceptorfor all three listener modes: TLS with a provided certificate, self-signed TLS, and plain HTTP.🤖 Generated with Claude Code