Skip to content

Limits and timeouts

etemenanki-app bounds every stage of a connection: how long a client may take to authenticate, how long a relay may sit idle, how many connections one listener holds, how large a message may be. Most of these values are fixed in the code and sized well above normal traffic. A few belong to one protocol and are settings.

Read this page when a connection closes and you want to know which timer closed it, when you size a server, or when you need to know whether UDP gets through a particular inbound and outbound. Each section says what happens at the limit and what the log shows.

Eleven keys change a limit. They all belong to one protocol, or to a balancer, and each is described in full on that page. Every other value on this page is fixed.

Key Where Default Accepted values
max_connections hysteria2 inbound settings 4096 1 or more
max_circuits hysteria2 inbound settings 65536 1 or more
udp_idle_timeout hysteria2 inbound settings 60 (seconds) 2 to 600, and only with udp = true
max_concurrent_streams hysteria2 outbound settings 102400 1 or more, no upper bound
mtu tun inbound settings 1500 1280 to 65535
udp_idle_timeout tun inbound settings 60 (seconds) any non-negative integer; not range-checked, and 0 breaks UDP
max_flows tun inbound settings 65536 any non-negative integer; 0 admits nothing
mtu wireguard outbound settings 1420 any non-negative integer; not range-checked
keepalive wireguard outbound settings unset (off) 0 to 65535 seconds; 0 is off
probe_interval [[balancer]] 30 (seconds) any non-negative integer
probe_timeout [[balancer]] 5 (seconds) any non-negative integer

All of them are plain integers. Durations are whole seconds, so probe_interval = "30s" is a type error, not 30 seconds:

configuration invalid: TOML parse error at line 8, column 18
|
8 | probe_interval = "30s"
| ^^^^^
invalid type: string "30s", expected u64

A value outside the integer type is a parse error too, for example mtu = 70000 on a TUN inbound: inbound tun-in: invalid settings: invalid value: integer `70000`, expected u16.

The defaults suit most deployments. This pair shows every configurable limit set explicitly, with the reason for each value in a comment. Both files pass etemenanki-app --test.

server.toml
# A Hysteria 2 server sized for a small group of users.
[[inbound]]
tag = "hy2-in"
protocol = "hysteria2"
listen = "0.0.0.0"
port = 443
[inbound.settings]
cert_file = "/etc/etemenanki/cert.pem"
key_file = "/etc/etemenanki/key.pem"
password = "replace-with-a-long-random-password"
udp = true
udp_idle_timeout = 120 # keep quiet UDP sessions for 2 minutes; 2 to 600
max_connections = 512 # QUIC connections; default 4096
max_circuits = 16384 # TCP streams plus UDP associations; default 65536
[[outbound]]
tag = "direct"
protocol = "freedom"

A TCP connection through a stream inbound (SOCKS, HTTP, Trojan, VLESS, VMess or Shadowsocks) passes these stages, and each stage has its own bound:

flowchart LR
  A["Accept: free handshake and connection slots"] --> T["Transport handshake: 10 s"]
  T --> P["Protocol request: 10 s"]
  P --> S["Sniffing: 300 ms or 4 KiB"]
  S --> D["Dial: DNS 5 s per query, TCP connect 10 s per address"]
  D --> R["Relay: closed after 300 s idle"]
  R -.-> K["Dead-peer checks: TCP keepalive, WebSocket and HTTP/2 pings"]
  • The transport handshake exists only when the inbound has a TLS, WebSocket or gRPC transport. A plain TCP inbound goes straight to the protocol request.
  • Sniffing runs only when sniffing is on and the client asked for an IP address. See inbounds.
  • The protocol request limit ends as soon as the inbound has the destination. On HTTP, Trojan, VLESS, VMess and Shadowsocks inbounds the connection is then under the relay idle limit, including while the outbound dials.
  • The dead-peer checks run for the whole life of the connection. They notice a peer that vanished without closing the connection, not one that is idle but still answering.

Hysteria 2 and TUN inbounds accept connections differently and have their own limits, described in Hysteria 2 and TUN.

Timeout Value Applies to What happens
Transport handshake 10 s Inbounds with network = "tls", "ws" or "grpc": the TLS handshake, the WebSocket upgrade, or the HTTP/2 preface The socket closes. Debug log: inbound transport failed: tls handshake timed out, with websocket or grpc in place of tls
Protocol request, silence 10 s HTTP, Trojan, VLESS, VMess and Shadowsocks inbounds: any 10 s without progress before the request is complete The connection closes: inbound handshake timed out after 10s
Protocol request, total 10 s from the client’s first byte The same inbounds The connection closes: client did not complete its request in time
SOCKS negotiation 10 s SOCKS inbound: greeting, authentication and request together The connection closes: client did not complete its request in time
TUN first byte 10 s TCP flows on a TUN inbound. A flow is dialed only after the client sends something, so a protocol where the server speaks first, such as SMTP or MySQL, always ends here The flow closes: tun: the client never spoke
Sniffing window 300 ms or 4 KiB, whichever comes first Flows to an IP address with sniffing on The flow is routed on its address
DNS query 5 s per query The udp, tls and https backends of [dns]; A and AAAA are asked in parallel, each with its own 5 s That query fails with dns: query timed out; the lookup still succeeds if the other query returned an address
TCP connect 10 s per address freedom, and the connection to the server of socks, http, trojan, vless, vmess and shadowsocks outbounds The next resolved address is tried
Hysteria 2 connect 10 s per address hysteria2 outbound: QUIC handshake and authentication The next resolved address is tried. When none answers: hysteria2: no address answered (...)
Hysteria 2 stream open 5 s hysteria2 outbound: opening one proxy stream on the shared connection The flow fails: hysteria2: timed out opening a proxy stream
Tunnel TCP connect 10 s per address wireguard outbound: a TCP connection inside the tunnel The next address is tried
Balancer probe probe_timeout, default 5 s Each member of a [[balancer]], name resolution included The member counts as down

The two protocol-request rows work together: a client that connects and says nothing is dropped after 10 seconds, and one that starts its request has 10 seconds from its first byte to finish it. So once the transport handshake is done, a slow client can hold a connection for at most about 20 seconds before it authenticates. Both messages are logged at debug level as the reason a connection ended, for example vless connection from Some(198.51.100.20) ended: inbound handshake timed out after 10s.

The system DNS backend has no timeout of its own: getaddrinfo applies the host resolver’s timeouts and retries.

Addresses are tried one after another, not raced. A name that resolves to three unreachable addresses takes 30 seconds to fail, and the error lists every attempt:

failed to connect to any address (192.0.2.10:443: connect to 192.0.2.10:443 timed out; 192.0.2.11:443: connect to 192.0.2.11:443 timed out; 192.0.2.12:443: connect to 192.0.2.12:443 timed out)

If a name has an address you cannot reach, such as an AAAA record on a host without working IPv6, set address_family on the outbound so it is skipped. See outbounds.

Timer Value Applies to What happens
Relay idle 300 s Connections on HTTP, Trojan, VLESS, VMess and Shadowsocks inbounds (for mux.cool, the carrier connection), Hysteria 2 streams, and TUN TCP flows and UDP associations: nothing read in either direction for 300 s The connection closes
SOCKS relay idle checked every 300 s SOCKS CONNECT relays A relay that moved no bytes since the previous check closes with socks: relay idle, between 300 and 600 s after its last byte
SOCKS UDP association idle 300 s SOCKS UDP ASSOCIATE: no datagram in either direction The association ends
TCP keepalive first probe after 120 s of silence, then every 30 s; 3 unanswered probes drop the connection Every TCP connection an inbound accepts, and every connection an outbound dials to its proxy server A vanished peer is detected about 210 s after it went silent
WebSocket ping after 60 s without a frame sent or received WebSocket sessions, both sides A Ping is sent
WebSocket idle 300 s without a frame sent or received WebSocket sessions, both sides The session reads as closed
HTTP/2 ping every 60 s; the PONG must arrive within 20 s gRPC inbound connections The HTTP/2 connection and all its streams close
HTTP/2 idle 300 s with no open stream gRPC inbound connections The HTTP/2 connection closes
QUIC idle 30 s Hysteria 2, both sides The QUIC connection closes
QUIC keepalive every 10 s hysteria2 outbound Keeps the connection alive while it is idle
Hysteria 2 UDP association udp_idle_timeout, default 60 s, checked once per second hysteria2 inbound with udp = true The association ends
TUN UDP flow udp_idle_timeout, default 60 s, no packet in either direction One client source talking to one peer on a TUN inbound The flow is retired. When every flow of a client source is retired, its association ends and frees its max_flows slot
WireGuard keepalive keepalive, off by default wireguard outbound An empty packet keeps the NAT mapping open

“Idle” means no payload. The relay idle timer restarts every time bytes arrive from the client or from the outbound, so a connection that carries a keepalive of its own, such as SSH’s ServerAliveInterval or a WebSocket application ping inside the tunnel, never reaches it. A long-poll or a quiet database connection that sends nothing for five minutes does.

TCP keepalive is set on sockets the proxy accepts and on sockets it dials to a proxy server. Connections that freedom opens directly to destinations use the operating system’s default, which on Linux is no keepalive. Unix-socket listeners have no TCP keepalive.

The WebSocket and HTTP/2 pings are answered by any live peer, so they never close a connection that is only idle. On the dialing side, each flow over gRPC opens its own HTTP/2 connection, which relies on TCP keepalive and the relay idle limit.

What Value Details
accept() failure on a listener 100 ms pause For example when the process runs out of file descriptors. The log says accept error, backing off 100ms: Too many open files (os error 24)
hysteria2 outbound reconnect 2 s, doubling up to 30 s After a failed connect, or a connection that lived less than 10 s. A connection that lived longer is replaced at once. Flows that arrive during the wait fail with hysteria2: connection is down, waiting before the next attempt
wireguard outbound rebuild 2 s, doubling up to 30 s Same rule as Hysteria 2. Flows that arrive during the wait fail with wireguard: tunnel is down, waiting before the next attempt
Balancer probe probe_interval, default 30 s The wait after each probe ends, per member

Every inbound that accepts sockets, that is every protocol except hysteria2 and tun, on TCP or on a Unix socket, has two fixed limits, counted per inbound:

Limit Value Counts At the limit
Live connections 65,536 Accepted sockets, from accept until close. A gRPC connection counts once, however many streams it carries A new connection is closed as soon as it is accepted. Debug log: dropping inbound connection; live connection limit reached
Concurrent handshakes 2,048 Connections still in their handshake A new connection is closed as soon as it is accepted. Debug log: dropping inbound connection; handshake limit reached

Both are guardrails against floods, not quotas. In practice the process’s open-file limit is reached first: each proxied connection holds one descriptor for the client and usually one for the outbound. Under systemd, set LimitNOFILE= as shown in running etemenanki-app.

Trojan, VLESS and VMess inbounds accept Xray’s mux.cool multiplexing automatically.

Limit Value At the limit
Sub-flows per carrier connection 256 A new sub-flow is ended at once; the carrier and its other sub-flows keep running
Data block per frame 8 KiB, as in Xray The carrier closes: mux: data length N exceeds 8192
Frame metadata 512 bytes, as in Xray The carrier closes: mux: metadata length N exceeds 512

UDP is routed packet by packet, and each association keeps one sub-link per outbound it has used. An association holds at most 64 sub-links; the 65th closes the one it sent to least recently. You reach this only with more than 64 distinct outbounds in one association. Outbounds explains the fan-out in detail.

Limit Value Side Counts At the limit
max_connections default 4096 inbound QUIC connections on the listener, authenticated or not The QUIC handshake is refused. Debug log: hysteria2: refusing a connection; the listener is full
max_circuits default 65,536 inbound TCP streams plus UDP associations, across all connections on the listener A new stream is reset (hysteria2: refusing a stream; the listener is at its circuit limit); a new association’s packets are dropped
Streams per client connection 1024 inbound Open bidirectional streams on one QUIC connection The client waits for QUIC stream credit
UDP associations per connection 256 both Live UDP sessions on one QUIC connection Inbound: packets for a new session are dropped. Outbound: hysteria2: connection is at its UDP association limit
max_concurrent_streams default 102,400 outbound Open proxy streams on the outbound’s one shared connection, for all users together The flow fails at once: hysteria2: connection is at its concurrent-stream limit
Receive windows 8 MiB per stream, 20 MiB per connection both Flow control The sender waits
UDP packet after reassembly 4096 bytes both Fragments are reassembled to at most this size The packet is dropped

A circuit holds its max_circuits slot for its whole life, until its relay ends or its association times out.

The two stream limits interact on the outbound. An etemenanki-app server grants each connection 1024 streams. With the default max_concurrent_streams = 102400, the outbound never refuses a stream itself: the 1025th concurrent flow waits for the server’s credit and fails after 5 seconds with hysteria2: timed out opening a proxy stream. Setting max_concurrent_streams to the server’s limit, as in the client example above, makes such a flow fail immediately instead. The Hysteria 2 page has the other Hysteria-specific values.

Limit Value Counts At the limit
max_flows default 65,536 TCP connections, plus one UDP association per client source address and port The new TCP connection or association is dropped. Debug log: tun: dropping a flow; the flow limit is reached
New peers queued per UDP source 16 Peers a client source has just addressed that its association has not picked up yet The first packet to a further new peer is dropped; the client’s retransmission gets through once the queue drains

A routing loop, where an outbound’s own traffic is steered back into the device, fills max_flows quickly. The TUN page explains how to keep the proxy’s own traffic off the device.

Every flow routed to a wireguard outbound shares one tunnel and one driver task, but each flow has its own fixed buffers between the application and the tunnel. None of them is configurable.

Buffer Value Per At the limit
TCP socket buffers 64 KiB in each direction Tunnel TCP connection Uplink: further writes stay in the channel. Downlink: the remote waits for the tunnel’s TCP window
TCP channel 256 writes toward the tunnel, 256 reads toward the client, plus at most one write the driver has taken but the socket has not accepted Tunnel TCP connection Uplink: the next write waits until the remote reads again. Downlink: the driver stops reading the socket until the client reads
UDP rings 64 KiB and 64 datagrams in each direction UDP association Send: a datagram that finds the ring full waits for the next pass. A datagram larger than the whole ring, or to an unaddressable destination such as port 0, is dropped. Receive: a datagram that arrives while the ring is full is dropped
UDP channel 256 datagrams in each direction, plus at most one held datagram toward the tunnel UDP association Uplink: the sender waits. Downlink: the driver stops reading the ring until the client reads

At the limit, only the stalled flow waits: every other flow on the same tunnel keeps moving. A dropped outgoing datagram is logged only at trace level, as wireguard: dropping a N-byte datagram to <addr>: <error>. One slow flow on the WireGuard page walks through the same buffers in order.

What Value
Cached names 8192; when full, entries are evicted whatever their TTL
TTL from a DNS answer (udp, tls, https) Clamped to 5 s minimum, 3600 s maximum
TTL for the system backend 60 s, since getaddrinfo reports none
Failed lookups Not cached

None of these is configurable. DNS covers the resolver in full.

Limit Value Side At the limit
WebSocket message and frame 1 MiB both The session fails
WebSocket early data 16 KiB both Inbound: the upgrade is answered 413 Payload Too Large. Outbound: an ?ed= value above 16 KiB is capped
gRPC message 1 MiB both The stream fails: grpc message length exceeds the accepted maximum
HTTP/2 concurrent streams 256 per connection inbound Announced to the client in SETTINGS; a compliant client waits for a free stream
HTTP/2 windows and frames 4 MiB per stream, 16 MiB per connection, 256 KiB frame both Flow control; the sender waits

Tunneled payloads are far smaller than these caps. The caps stop a hostile peer from making the server buffer a message of its own choosing. Transports describes each carrier.

Limit Value Applies to At the limit
HTTP request head 64 KiB, at most 128 headers http inbound The connection closes: http head exceeds maximum size, or malformed http request: too many headers
Trojan UDP payload 8192 bytes per packet trojan inbound The association ends: trojan: oversize payload
Reply datagram delivered whole 8192 bytes for Trojan, VLESS and VMess; 4096 bytes for Hysteria 2 and TUN; 64 KiB for SOCKS UDP replies from an outbound to the client Longer replies are truncated, as a kernel recv into a short buffer would
VMess clock skew 120 s either way vmess inbound The client cannot authenticate; see VMess
Shadowsocks 2022 clock skew 30 s either way shadowsocks with a 2022- method The request is refused: shadowsocks-2022: bad timestamp

UDP support depends on both ends: the inbound must relay datagrams, and the outbound each packet is routed to must carry them.

Protocol As an inbound As an outbound
socks Yes, by UDP ASSOCIATE when udp = true (the default); SOCKS4 has no UDP Yes, by UDP ASSOCIATE; the datagrams go to the server’s relay port as plain UDP
http No No
shadowsocks (all methods) No No
trojan Yes, always on; also mux.cool with XUDP Yes, inside the Trojan connection
vless Yes, always on; also mux.cool with XUDP Yes, inside the VLESS connection. The framing has no per-packet address, so every packet goes to the destination of the packet that opened the sub-link
vmess Yes, always on; also mux.cool with XUDP Yes, inside the VMess connection, with the same one-destination rule as VLESS
hysteria2 Only with udp = true (off by default) Yes, as QUIC datagrams, if the server allows UDP
tun Yes when udp = true (the default) not an outbound
wireguard not an inbound Yes
freedom not an inbound Yes, one socket per address family
blackhole not an inbound Accepted and discarded
a balancer not an inbound As its chosen member

What happens when the two ends do not match:

  • The inbound has no UDP. An HTTP or Shadowsocks client has no way to ask for UDP. A SOCKS5 client asking for UDP ASSOCIATE on an inbound with udp = false gets “command not supported”. A Hysteria 2 server with udp = false tells each client at authentication that it does not relay UDP.
  • The outbound has no UDP. The router does not know which outbounds carry datagrams. A packet routed to http or shadowsocks is dropped, and the association’s other packets carry on. The debug log shows udp fan-out: opening an outbound failed: http carries no datagrams, or shadowsocks / shadowsocks-2022 in place of http. Put a network = "udp" rule before the rule that would send UDP there.
  • The Hysteria 2 server refuses UDP. Packets routed to a hysteria2 outbound whose server has UDP off are dropped. The outbound logs one warning, and the debug log shows udp fan-out: opening an outbound failed: hysteria2: the server does not relay UDP.

Rules see each UDP packet on its own; see routing and how UDP reaches an outbound.

Configuration errors appear when the file is loaded or checked with --test, prefixed with configuration invalid:. Run-time messages are logged at debug level unless noted, so raise [log].level to see them.

Message Cause Fix
inbound TAG: max_connections must be at least 1 (or max_circuits) A Hysteria 2 limit is 0 Remove the key for the default, or set 1 or more
inbound TAG: udp_idle_timeout must be between 2 and 600 seconds Hysteria 2 udp_idle_timeout out of range Use 2 to 600
inbound TAG: udp_idle_timeout is set but udp is not enabled udp_idle_timeout on a Hysteria 2 inbound without udp = true Add udp = true or remove the key
outbound TAG: max_concurrent_streams must be at least 1 max_concurrent_streams = 0 Remove the key or set 1 or more
inbound TAG: tun mtu must be at least 1280 TUN mtu below 1280 Use 1280 or more
invalid type: string "30s", expected u64 A duration written as a string Write whole seconds: probe_interval = 30
accept error, backing off 100ms: Too many open files (os error 24) (warning) The process ran out of file descriptors Raise the open-file limit, for example LimitNOFILE= under systemd
dropping inbound connection; live connection limit reached 65,536 live connections on one inbound Spread clients over several inbounds or hosts
dropping inbound connection; handshake limit reached 2,048 connections in their handshake at once, usually a scan or flood Filter the source in a firewall
inbound handshake timed out after 10s A client connected and sent nothing, or stalled for 10 s Check the client’s protocol, port and transport settings
client did not complete its request in time A client started its request and did not finish it within 10 s A stalled or very slow client, or one that sends only part of the request
failed to connect to any address (...) Every resolved address refused or timed out after 10 s Check the destination, or restrict address_family
dns: query timed out The DNS server did not answer within 5 s Check [dns].server and the path to it
hysteria2: timed out opening a proxy stream The server withheld stream credit for 5 s Lower max_concurrent_streams to fail fast, or add a second outbound
hysteria2: connection is at its concurrent-stream limit max_concurrent_streams flows are open on the outbound Raise max_concurrent_streams
hysteria2: refusing a connection; the listener is full max_connections QUIC connections are live Raise max_connections
hysteria2: refusing a stream; the listener is at its circuit limit max_circuits circuits are live Raise max_circuits
tun: dropping a flow; the flow limit is reached max_flows flows are live, or a routing loop Fix the loop, or raise max_flows
grpc message length exceeds the accepted maximum The peer sent a gRPC message over 1 MiB The peer is not a standard gRPC tunnel client