DocsReferenceLimits & timeouts

Limits & timeouts

Every cap in this page is flag-overridable, but the defaults are chosen deliberately — each one guards against a specific failure mode, not a round number picked for its own sake.

HTTP server timeouts#

ReadHeaderTimeoutdurationdefault: 10s
Guards against slowloris-style clients that trickle headers in one byte at a time.
IdleTimeoutdurationdefault: 120s
Public keep-alive idle timeout. Guards against file-descriptor exhaustion from idle connections.
MaxHeaderBytesbytesdefault: 64 KiB
Guards against header bombs.
ResponseHeaderTimeoutdurationdefault: 30s
How long the edge waits for the local app to start responding before returning 504. Guards against a hung local app. Post-header response duration is unlimited by design — streaming downloads and SSE need that, and a client disconnect cancels the request context anyway.
Dial-to-agentdurationdefault: 5s
Budget for opening the yamux stream and writing its preamble. Times out to 502 if the session is wedged.

Body limits#

Declared body capbytesdefault: 256 MiB
Checked against Content-Length before the request is proxied at all — this is the real 413, and it covers every well-behaved client.
Streaming body capbytesdefault: 256 MiB
Enforced via MaxBytesReader for chunked or unknown-length bodies, surfaced as 413 from a custom ErrorHandler that type-checks *http.MaxBytesError.

A chunked 413 may ship a partial body

The naive design — relying on MaxBytesReader alone — returns 502, not 413, because the trip surfaces as a request-body read error inside RoundTrip after headers and up to 256 MiB have already reached the agent. With both checks in place the chunked path still means the agent may have already forwarded a partial body to the local app before the edge cuts it off.

Rate limiting#

Per-IP rate limittoken bucketdefault: 20 req/s, burst 50
Backed by an LRU of 10k IPs. Exceeding it returns 429.
Auth attemptsratedefault: 5 / IP / minute
Applies to the control-plane authentication handshake on :4443.

Concurrency & the window budget#

Global in-flight semaphoreintdefault: 64
The total memory ceiling for the system — see the callout below.
Per-session concurrent streamsintdefault: 48
Its own semaphore, beneath the global one, so one tunnel can't monopolize the edge. Exceeding it returns 429.
yamux AcceptBacklogintdefault: 512
Deliberately set above both caps — see the callout below.
Peak tunnel bufferingbytesdefault: 64 MiB per side
64 concurrent streams × a 1 MiB yamux receive window each — the largest buffer in the system.

AcceptBacklog is not the concurrency cap

It's an easy and expensive misreading. AcceptBacklog is the depth of yamux's accept queue; when it's full, a new stream gets RST'd — a 502the client can't interpret, instead of the 429 the system intends. Concurrency is enforced by the per-session (48) and global (64) semaphores instead, with AcceptBacklog set comfortably above both (512) so it never becomes the binding constraint. The yamux receive window is itself a buffer — each open stream may hold up to its window of received-but-unread bytes on both machines, so concurrency × window is the number that matters, not either alone.

Control-plane caps#

:4443 concurrent TLS handshakesintdefault: 16
Guards against an unauthenticated handshake flood.
:4443 unauthenticated sessionsintdefault: 32
Guards against slow-auth session hoarding.