tcp_connect blocks on TCPResponse (no fast-open) → fixed ~3s latency per connection
Environment
- crate: hysteria2 0.1.6
- server: sing-box (hysteria2 inbound)
Symptom
When using this crate as a client, every newly established proxied connection has a fixed ~3 second delay before its first response. The official Go client (apernet/hysteria) against the same server takes only tens of milliseconds.
A/B comparison (same server, same target):
| client | first response (single conn) |
|---|---|
| official Go client | ~64 ms |
| this crate | ~3.1 s |
20 serial connections: ~62s with this crate, ~1.7s after the fix below.
So it's unrelated to quinn, rate limiting, MTU, or congestion control — it's purely a connection-setup ordering problem.
Root cause
tcp_connect synchronously reads and parses the server's TCPResponse before
returning the stream.
But in hysteria2's design many servers (e.g. sing-box) send the TCPResponse lazily — they wait until the upstream target connection is established, or until they receive the client's first bytes, before replying with the TCPResponse.
This creates a circular wait:
- client: won't return the stream until it has the TCPResponse, so the caller can't write its first bytes;
- server: won't send the TCPResponse until it receives the first bytes (or the upstream is ready).
Both sides wait on each other until the server's ~3s fallback timeout breaks the deadlock — hence the constant ~3.1s first-response time per connection. Any caller protocol of the "connect succeeds first, then send the request" kind (very common) triggers this reliably.
How the official client avoids it: fast-open
The official Go client uses fast-open: tcp_connect returns the stream
immediately and does not wait for the TCPResponse; the actual TCPResponse is
stripped lazily on the stream's first read. This lets the caller write its first bytes
right away and breaks the circular wait.
Suggestion
Add a fast-open option to tcp_connect (or make it the default behavior): after
sending the TCP request during the handshake, return the stream immediately and defer
TCPResponse parsing until the first read.
With this change applied locally, single-connection first response dropped from 3.13s to 0.065s.
Source: i18n-site/rust