Skip to content

Limits and escape hatches ​

For CLI reverse-proxy operators and library users. Every limit that can reject a legitimate request has a key. A rejection is logged at Error with the limit, the observed value, and the key that raises it.

The default log level is Error. Run titanium run -c twp.yaml -v or set logging.minimumLevel to Debug or Information when you need Observe-mode lines. Observe logs at Debug. A rejected request logs at Error so it shows up without -v.

Behavior changes at the previous defaults ​

  1. Origin HTTP/2 responses whose headers are between 8 KiB and 64 KiB now arrive intact. Above 64 KiB the stream fails with 502 and names server.limits.maxDecodedHeaderListBytes. Previously those headers were truncated in silence.
  2. Under Balanced (the default profile), an HTTP/1 request or origin response with more than 256 headers or 256 KiB of header bytes is rejected: 431 for a request, 502 for an origin response. LegacyCompatible keeps HeaderLimits at Observe, so the same message is logged and passed.
  3. Limit-driven failures appear in the default Error log. The line names the property, the configured limit, the observed value, and the CLI key.

How a rejection looks ​

text
headerLimits (Enforce): header count exceeded MaxHeaderCount=256; observed 300; client got 431. Raise server.limits.maxHeaderCount or set server.policyModes.headerLimits to observe or disabled. host cdn.example

The first line for a limit is immediate. Further hits of that same limit are folded into one line per 10 seconds, with a suppressed-count suffix. Metrics count every rejection. Logs never include URLs, header values, cookies, or bodies. The host name is the only request identity on the line.

Policy modes ​

server.policyModes.<family> is Enforce, Observe, or Disabled.

ModeEffect
EnforceReject the request and log at Error.
ObserveLet the request through and log at Debug.
DisabledSkip the check.

Families: bodyBudget, decompressionRatio, headerLimits, admissionControl, http2AbuseBudget, http2RelayValidation, webSocketFrameBudget, http1ReplaySafety. allowAmbiguousFraming is a separate boolean. A partial policyModes block changes only the keys you set. Under PublicFacing, leaving http1ReplaySafety unset keeps Enforce.

Buffering and streaming ​

maxBufferedBodyBytes applies when something materializes a whole body (GetRequestBody, GetResponseBody, a hook that needs the bytes). A streaming relay does not buffer the body and does not consult that cap. CanBufferBody reports whether a message is small enough to buffer. webSocketFrameBudget is the per-frame WebSocket cap (maxWebSocketFramePayloadBytes); a frame over it closes with 1009. Filtering hooks still see a whole frame. The raw relay (no subscriber) does not decode frames.

maxEncodedBodyBytes, maxDecodedBodyBytes, and maxDecompressionRatio are accepted and reserved, not enforced. Body expansion is bounded by maxBufferedBodyBytes. Startup warns when you set them.

Profiles ​

ProfileHeader limitsStreams / connectionIdle / request timeoutWho it is for
Balanced (default)Enforce, 256 headers / 256 KiB1000disabled (0)Single-user and trusted clients
LegacyCompatibleObserve1000disabled4.x migrations
PublicFacingEnforce256idle 60 s, request 120 sUntrusted clients

PublicFacing idle and request deadlines cut long-lived SSE, gRPC streaming, and WebSocket sessions. Raise server.timeouts.idleReadTimeoutSeconds, idleWriteTimeoutSeconds, and requestTimeoutSeconds, or set them to 0, when those protocols must stay open. 256 concurrent streams is the PublicFacing cap, not the Balanced default.

Reference ​

What failsDefaultCeilingCLI key
Buffered body4 MiB—server.limits.maxBufferedBodyBytes
Header count256—server.limits.maxHeaderCount
Header block size256 KiB—server.limits.maxHeaderAggregateBytes
Header line64 KiB—server.limits.maxHeaderLineBytes
HTTP/3 frame payload4 MiB64 MiBserver.limits.maxHttp3FramePayloadBytes
HTTP/2 deferred DATA per stream4 MiB + 16 KiB64 MiBserver.limits.maxDeferredOutboundBytesPerStream
Decoded header list64 KiB—server.limits.maxDecodedHeaderListBytes
Trailer count10010000server.limits.maxTrailerHeaderCount
Trailer block16 KiB1 MiBserver.limits.maxTrailerHeaderBlockBytes
Compressed HTTP/2 header block256 KiB16 MiBserver.limits.maxHttp2CompressedHeaderBlockBytes
Interim 1xx responses20100server.limits.maxInterimResponses
Auth challenge rounds320server.limits.maxAuthChallengeRounds
Upstream proxy auth attempts520server.limits.maxUpstreamProxyAuthenticationAttempts
Windows auth token12288 bytes1 MiBserver.limits.maxWinAuthTokenBytes
HTTP/2 flow-control wait60 s3600 sserver.limits.http2WindowUpdateTimeoutSeconds
Client connections (process)unset—server.pooling.maxConcurrentClientConnections
Client connections (endpoint)unset—server.limits.maxConcurrentClients
Connect timeout0 (off)—server.timeouts.connectTimeOutSeconds
Response header timeout0 (off)—server.timeouts.responseHeaderTimeoutSeconds
Idle read timeout0 (off)—server.timeouts.idleReadTimeoutSeconds
Idle write timeout0 (off)—server.timeouts.idleWriteTimeoutSeconds
Request timeout0 (off)—server.timeouts.requestTimeoutSeconds
Client header timeout0 (off)—server.timeouts.clientHeaderTimeoutSeconds
WebSocket frame16 MiB—server.limits.maxWebSocketFramePayloadBytes

Per-route overrides (request timeout, idle timeout, buffered body, WebSocket frame) live on routes[].limits and apply only to the matched session. Omit the block and the server limits are unchanged.

Memory ​

HTTP/3 frame reads rent at most 256 KiB until bytes arrive, then grow. A peer that declares a huge length and sends nothing cannot reserve the ceiling times the stream count. Worst case for parked HTTP/2 DATA is concurrent streams times maxDeferredOutboundBytesPerStream.

Fixed on purpose ​

These stay constant. They are protocol limits or anti-wrap guards, not traffic you should raise:

  • HTTP/2 MAX_FRAME_SIZE we accept is 16384. We do not advertise a larger value.
  • HTTP/3 control-stream frames are 16 KiB.
  • QPACK acknowledgement capacity.
  • The 1 GiB chunk-size guard (stops a wrapped chunk length).
  • SOCKS hostnames are 255 bytes (RFC 1928).

414 and 421 are not produced by these limits. 431 is the request header-count or header-block rejection. 413 is the buffered-body rejection.

See also ​

Core, CLI, and website: MIT