#5·rust

tcp_connect blocks on TCPResponse (no fast-open) → fixed ~3s latency per connection

Author: trustrustyCreated Jun 11, 2026Updated Jun 11, 2026

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.