Skip to content

Limits, timeouts and memory

Source files: 108 · checked against Etemenanki 596916d
  • Etemenanki/concepts/src/core.rs
  • Etemenanki/concepts/src/runtime.rs
  • Etemenanki/concepts/src/client.rs
  • Etemenanki/concepts/src/buffer.rs
  • Etemenanki/concepts/src/relay.rs
  • Etemenanki/concepts/src/wake.rs
  • Etemenanki/protocols/src/core/mod.rs
  • Etemenanki/protocols/src/core/harness.rs
  • Etemenanki/protocols/src/sniff/mod.rs
  • Etemenanki/protocols/src/sniff/collector.rs
  • Etemenanki/protocols/src/transports/accept.rs
  • Etemenanki/protocols/src/transports/connect.rs
  • Etemenanki/protocols/src/transports/keepalive.rs
  • Etemenanki/protocols/src/transports/ws/endpoint.rs
  • Etemenanki/protocols/src/transports/ws/stream.rs
  • Etemenanki/protocols/src/transports/grpc/settings.rs
  • Etemenanki/protocols/src/transports/grpc/liveness.rs
  • Etemenanki/protocols/src/transports/grpc/framing.rs
  • Etemenanki/environment/src/dial/tcp.rs
  • Etemenanki/environment/src/dial/socket.rs
  • Etemenanki/protocols/src/mux/demux.rs
  • Etemenanki/protocols/src/mux/frame.rs
  • Etemenanki/protocols/src/helpers/address.rs
  • Etemenanki/protocols/src/http/protocol.rs
  • Etemenanki/protocols/src/http/core.rs
  • Etemenanki/protocols/src/http/codec.rs
  • Etemenanki/protocols/src/socks/server.rs
  • Etemenanki/protocols/src/socks/protocol.rs
  • Etemenanki/protocols/src/socks/codec.rs
  • Etemenanki/protocols/src/socks/udp_link.rs
  • Etemenanki/protocols/src/trojan/protocol.rs
  • Etemenanki/protocols/src/trojan/core.rs
  • Etemenanki/protocols/src/trojan/codec.rs
  • Etemenanki/protocols/src/vless/protocol.rs
  • Etemenanki/protocols/src/vless/core.rs
  • Etemenanki/protocols/src/vless/codec.rs
  • Etemenanki/protocols/src/vmess/core.rs
  • Etemenanki/protocols/src/vmess/codec.rs
  • Etemenanki/protocols/src/vmess/framing.rs
  • Etemenanki/protocols/src/vmess/accounts.rs
  • Etemenanki/protocols/src/ss_legacy/aead.rs
  • Etemenanki/protocols/src/ss_legacy/core.rs
  • Etemenanki/protocols/src/ss_legacy/codec.rs
  • Etemenanki/protocols/src/ss_2022/crypto.rs
  • Etemenanki/protocols/src/ss_2022/protocol.rs
  • Etemenanki/protocols/src/ss_2022/core.rs
  • Etemenanki/protocols/src/ss_2022/codec.rs
  • Etemenanki/protocols/src/hysteria/config.rs
  • Etemenanki/protocols/src/hysteria/protocol.rs
  • Etemenanki/protocols/src/hysteria/connection.rs
  • Etemenanki/protocols/src/hysteria/slot.rs
  • Etemenanki/protocols/src/hysteria/server/config.rs
  • Etemenanki/protocols/src/hysteria/server/endpoint.rs
  • Etemenanki/protocols/src/hysteria/server/inbound.rs
  • Etemenanki/protocols/src/hysteria/server/datagrams.rs
  • Etemenanki/protocols/src/hysteria/server/shim.rs
  • Etemenanki/protocols/src/hysteria/obfs.rs
  • Etemenanki/protocols/src/wireguard/config.rs
  • Etemenanki/protocols/src/wireguard/device.rs
  • Etemenanki/protocols/src/wireguard/slot.rs
  • Etemenanki/protocols/src/tun/config.rs
  • Etemenanki/protocols/src/tun/inbound.rs
  • Etemenanki/protocols/src/tun/udp.rs
  • Etemenanki/protocols/src/dns/mod.rs
  • Etemenanki/protocols/src/dns/message.rs
  • Etemenanki/app/src/serve.rs
  • Etemenanki/app/src/connector.rs
  • Etemenanki/app/src/outbound/mod.rs
  • Etemenanki/app/src/outbound/proxy.rs
  • Etemenanki/app/src/outbound/freedom.rs
  • Etemenanki/app/src/outbound/udp_fanout.rs
  • Etemenanki/app/src/balancer.rs
  • Etemenanki/app/src/instance.rs
  • Etemenanki/app/src/inbound/mod.rs
  • Etemenanki/app/src/inbound/tun.rs
  • Etemenanki/app/src/config.rs
  • Etemenanki/app/src/main.rs
  • Etemenanki/concepts/tests/runtime.rs
  • Etemenanki/concepts/tests/client.rs
  • Etemenanki/environment/tests/integration/tcp.rs
  • Etemenanki/protocols/tests/unit/core/mod.rs
  • Etemenanki/protocols/tests/unit/sniff/collector.rs
  • Etemenanki/protocols/tests/unit/transports/accept.rs
  • Etemenanki/protocols/tests/unit/transports/grpc_framing.rs
  • Etemenanki/protocols/tests/unit/transports/grpc_liveness.rs
  • Etemenanki/protocols/tests/unit/transports/ws_endpoint.rs
  • Etemenanki/protocols/tests/unit/mux/demux.rs
  • Etemenanki/protocols/tests/unit/mux/frame.rs
  • Etemenanki/protocols/tests/unit/http/core.rs
  • Etemenanki/protocols/tests/unit/socks/server.rs
  • Etemenanki/protocols/tests/pipeline/socks.rs
  • Etemenanki/protocols/tests/unit/trojan/protocol.rs
  • Etemenanki/protocols/tests/unit/trojan/core.rs
  • Etemenanki/protocols/tests/unit/vmess/accounts.rs
  • Etemenanki/protocols/tests/unit/hysteria/protocol.rs
  • Etemenanki/protocols/tests/unit/hysteria/slot.rs
  • Etemenanki/protocols/tests/unit/hysteria/server/datagrams.rs
  • Etemenanki/protocols/tests/unit/wireguard/slot.rs
  • Etemenanki/protocols/tests/unit/wireguard/device.rs
  • Etemenanki/protocols/tests/pipeline/wireguard.rs
  • Etemenanki/protocols/tests/unit/tun/udp.rs
  • Etemenanki/protocols/tests/unit/dns/mod.rs
  • Etemenanki/app/tests/unit/inbound.rs
  • Etemenanki/app/tests/unit/outbound.rs
  • Etemenanki/app/tests/unit/serve.rs
  • Etemenanki/app/tests/unit/balancer.rs
  • Etemenanki/app/tests/integration/e2e_udp_route.rs
  • Etemenanki/app/tests/integration/e2e_balancer.rs

This page lists every timeout, cap and fixed buffer size in the Etemenanki kernel crates and in etemenanki-app. For each one it gives the Rust name, the value, the file that defines it and the mechanism that enforces it. The second half turns the buffer constants into a per-connection memory budget: what one proxied connection costs for each server core and each client codec.

Read it before you change a limit, add a protocol, or try to explain why a connection ended. The runtime, the cores, the transports, Hysteria 2, WireGuard and TUN each have their own page. This page covers only the numbers and where they apply. The few limits an operator can set in the configuration file are described for operators in Limits.

A limit in the code takes one of three forms:

Form Where it lives Who can change it
A const in a kernel or app module Next to the code that enforces it, usually pub or pub(crate) so tests can name it Only a code change and a release
A configuration default A DEFAULT_* constant that a builder in app/src/ uses when a key is absent The operator, through the keys in Configurable limits
A derived bound A const fn or a const expression over other constants, such as downlink_overhead(Self::BUF_SIZE) Only by changing its inputs

Two rules keep the buffer numbers consistent. Both are enforced in code:

  • The byte buffers of a runtime are fixed-size boxed arrays (Box<[u8; N]>, from boxed_array in concepts/src/buffer.rs), allocated and zeroed once, when the runtime is built. They never grow. Backpressure comes from a full buffer, not from a queue.
  • A runtime refuses to be built when its buffer cannot hold the core’s or codec’s staging reserve. ProxyServerRuntime::build asserts BUF_SIZE > Core::STAGING_RESERVE and ProxyClientRuntime::new asserts BUF_SIZE > Codec::STAGING_RESERVE. Both panic, with BUF_SIZE must exceed the core's STAGING_RESERVE or nothing is ever read and BUF_SIZE must exceed the codec's STAGING_RESERVE or nothing can be sealed.

BUF_SIZE is a const generic of both runtimes:

concepts/src/runtime.rs
pub struct ProxyServerRuntime<const BUF_SIZE: usize, Core, Trans, Conn, Mode = ProxyRunsQuiet>
where
Core: ProxyCoreDecode,
Conn: Connector<Core::Target>,
concepts/src/client.rs
pub struct ProxyClientRuntime<const BUF_SIZE: usize, Codec, Conn>
where
Codec: ProxyCoreEncodeHandshake,
Conn: Connector<Codec::Target>,
impl<const BUF_SIZE: usize, Codec, Conn> ProxyClientRuntime<BUF_SIZE, Codec, Conn>
where
Codec: ProxyCoreEncodeHandshake,
Codec::Error: Error + Send + Sync + 'static,
Conn: Connector<Codec::Target>,
{
pub fn new(codec: Codec, connector: &mut Conn, target: Codec::Target) -> Self
}

BUF_SIZE is not a trait item. Each server core exposes it as an inherent constant, for example pub const BUF_SIZE: usize = 16 * 1024 on impl<T> TrojanCore<T>. The caller instantiates the runtime with it, as in drive::<{ TrojanCore::<()>::BUF_SIZE }, _, _> in app/src/serve.rs. Client buffer sizes are chosen by the app, in app/src/outbound/mod.rs, as the first parameter of ProxyClient<const BUF: usize, S, D>.

The two reserve constants are trait items:

concepts/src/core.rs
pub trait ProxyCoreDecode {
type Key: Copy + Ord + Send + Sync + 'static;
type Target;
type Error;
type TransportAddr: Clone + Send + Sync + 'static;
const STAGING_RESERVE: usize;
const MAX_DATAGRAM: usize = 4096;
fn handle(
&mut self,
event: Event<'_, Self>,
effects: &mut Effects<'_, Self>,
) -> Result<usize, Self::Error>;
fn held(&self) -> &[u8] { &[] }
}
pub trait ProxyCoreEncodeHandshake {
type Target;
type Error;
const STAGING_RESERVE: usize;
fn start(&mut self, out: &mut Staging<'_>) -> Result<Handshake, Self::Error>;
fn reply(&mut self, wire: &mut [u8], out: &mut Staging<'_>) -> Result<Reply, Self::Error>;
fn finish(&mut self, out: &mut Staging<'_>) -> Result<(), Self::Error>;
}
  • STAGING_RESERVE on a server core is the most the core stages toward the transport while it handles one event, not counting the event’s own payload.
  • STAGING_RESERVE on a client codec is the most one start, reply, finish or seal call adds beyond the plaintext it takes: header, length prefix, tag and padding.
  • MAX_DATAGRAM is the largest packet delivered whole on either side of a server runtime. A longer packet is truncated, the way a kernel recv truncates.

The runtime turns the last two into the largest datagram it reads from a datagram outbound:

concepts/src/runtime.rs
impl<const BUF_SIZE: usize, Core, Trans, Conn, Mode>
ProxyServerRuntime<BUF_SIZE, Core, Trans, Conn, Mode>
where
Core: ProxyCoreDecode,
Trans: Transport<Addr = Core::TransportAddr>,
Conn: Connector<Core::Target>,
Conn::Datagram: DatagramLink<Addr = Destination>,
{
pub const fn datagram_limit() -> usize
}

datagram_limit() returns min(Core::MAX_DATAGRAM, BUF_SIZE - Core::STAGING_RESERVE).

Mux carriers size their reserve with one shared helper, so the Keep headers that one outbound read splits into always fit:

protocols/src/mux/demux.rs
pub const fn downlink_overhead(read_size: usize) -> usize

It returns read_size.div_ceil(MAX_DATA_LEN).saturating_mul(FRAME_OVERHEAD_MAX).

Every core keeps its one deadline in a Timing value from protocols/src/core/mod.rs:

protocols/src/core/mod.rs
pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
pub const RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
impl Timing {
pub fn new() -> Self
pub fn phase(&self) -> Phase
pub fn is_established(&self) -> bool
pub fn touch<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>)
pub fn enter<C: ProxyCoreDecode>(&mut self, phase: Phase, fx: &mut Effects<'_, C>)
pub fn expired<C: ProxyCoreDecode>(&mut self, fx: &mut Effects<'_, C>) -> Expired
}

A runtime delivers no event while the client is silent, so the app also watches the handshake from outside the runtime:

app/src/serve.rs
async fn drive<const BUF: usize, Core, S>(
stream: S,
core: Core,
connector: AppConnector,
handshake: Arc<OwnedSemaphorePermit>,
) -> io::Result<()>
where
S: AsyncRead + AsyncWrite + Unpin,
Core: ProxyCoreDecode<Target = Flow, Error = io::Error, TransportAddr = ()> + Established,
pub trait Established {
fn is_established(&self) -> bool;
}
protocols/src/transports/keepalive.rs
pub fn set_keepalive(stream: &TcpStream)
// environment/src/dial/tcp.rs
impl TcpDialer {
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self
pub async fn connect(&self, addr: SocketAddr) -> io::Result<TcpStream>
pub async fn connect_any(&self, addrs: &[SocketAddr]) -> io::Result<TcpStream>
}
// environment/src/dial/socket.rs
impl SocketOptions {
pub fn with_tcp_keepalive(mut self, idle: Duration) -> Self
}
// protocols/src/transports/ws/endpoint.rs
pub(crate) fn ws_config() -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig
// protocols/src/transports/grpc/liveness.rs
impl Liveness {
pub(crate) fn new(ping_pong: Option<PingPong>) -> Self
pub(crate) fn note_progress(&mut self)
pub(crate) async fn watch(&mut self, idle: bool)
}
// protocols/src/dns/mod.rs
impl Resolver {
pub fn with_ttl_bounds(self, min: Duration, max: Duration) -> Self
pub async fn resolve(&self, host: &str) -> io::Result<Vec<IpAddr>>
}
// protocols/src/hysteria/slot.rs (ConnSlot) and protocols/src/wireguard/slot.rs (DeviceSlot)
pub fn died_young(&self) -> bool
pub fn note_failure(&mut self)
// app/src/balancer.rs
impl Balancer {
pub fn spawn_probe(
self: &Arc<Self>,
token: CancellationToken,
interval: Duration,
timeout: Duration,
resolver: Resolver,
)
}

The diagram follows one TCP connection through etemenanki-app and marks where each group of limits applies. Hysteria 2 and TUN inbounds own their sockets and run their own admission; their sections below cover them.

flowchart TB
  accept["accept loop: ACCEPT_ERROR_BACKOFF"]
  admit["live permit, then handshake permit"]
  transport["InboundTransport::accept: keepalive, TRANSPORT_HANDSHAKE_TIMEOUT"]
  carrier["WS and gRPC: message caps, idle and PING deadlines"]
  runtime["ProxyServerRuntime: 3 x BUF_SIZE, FrameTooLarge"]
  timing["core Timing: handshake, sniff, idle"]
  watchdog["drive: HANDSHAKE_TIMEOUT per step until established"]
  mux["mux.cool Demux: MAX_SESSIONS, MAX_DATA_LEN"]
  connector["AppConnector"]
  tcp["freedom: DEFAULT_CONNECT_TIMEOUT per address"]
  fanout["UDP flow: FanOutLink, MAX_SUBS"]
  client["proxy outbound: ProxyClientRuntime, 2 x BUF"]
  dns["Resolver: QUERY_TIMEOUT, CACHE_CAPACITY"]
  accept --> admit --> transport --> carrier --> runtime
  runtime --> timing
  watchdog --> runtime
  runtime --> mux
  runtime --> connector
  mux --> connector
  connector --> tcp
  connector --> fanout
  connector --> client
  tcp --> dns

Each core has exactly one deadline, and its meaning depends on the phase. Timing::touch runs at the top of every byte-carrying event, and Timing::enter moves to a new phase and arms that phase’s deadline:

stateDiagram-v2
  [*] --> Handshake: runtime built, nothing armed
  Handshake --> Handshake: first byte event arms HANDSHAKE_TIMEOUT
  Handshake --> Sniff: request parsed, IP target, sniffing on
  Handshake --> Relay: request parsed, flow opened
  Sniff --> Relay: domain found, budget spent, or SNIFF_TIMEOUT
  Relay --> Relay: every byte event re-arms RELAY_IDLE_TIMEOUT
  Relay --> Closing: Expired Idle, Timing pushes Finish
  Relay --> [*]: both halves closed, core pushes Finish
  Closing --> [*]: runtime finishes
  Handshake --> [*]: Expired Handshake, core returns an error

No core enters Phase::Closing itself. Only Timing::expired moves a relaying connection there, in the same step that pushes Effect::Finish. When both halves of a single-flow relay close normally, Passthrough pushes Finish while Timing is still in Phase::Relay.

The two handshake guards complement each other:

Client behaviour What ends it Error text
Sends nothing at all drive wraps each runtime.next() in tokio::time::timeout(HANDSHAKE_TIMEOUT, …) until the core reports is_established() inbound handshake timed out after 10s
Sends a first byte, then trickles the rest Timing arms HANDSHAKE_TIMEOUT once, on the first byte event, and never refreshes it in Phase::Handshake, so it limits the whole request rather than the gap between bytes client did not complete its request in time

The idle deadline, by contrast, is re-armed on every byte event in Relay and Closing. That makes it an idle limit, not a lifetime limit.

Values are exact. Sizes are given in KiB or MiB where the source writes n * 1024.

Constant Value Defined in Enforced by
BUF_SIZE (const generic) Per core; see Server cores ProxyServerRuntime in concepts/src/runtime.rs Sizes the boxed arrays up (a ReadBuffer), staging (a WriteBuffer) and scratch.
ProxyCoreDecode::STAGING_RESERVE Per core concepts/src/core.rs poll_transport_read reads the transport only when staging.room() is at least the reserve. A stream outbound is read only with STAGING_RESERVE + 1 bytes of room, and then into at most room - STAGING_RESERVE bytes of scratch. A datagram outbound is read only with STAGING_RESERVE + datagram_limit() of room.
ProxyCoreDecode::MAX_DATAGRAM 4096 by default; some cores raise it concepts/src/core.rs A datagram outbound is read into scratch[..datagram_limit()]. A datagram transport is read into up.free()[..MAX_DATAGRAM.min(BUF_SIZE)].
RuntimeError::FrameTooLarge When the unparsed region fills BUF_SIZE concepts/src/runtime.rs, in poll_transport_read ReadBuffer::is_saturated() (start == 0 && end == N) while the core still wants more. Error text: protocol frame exceeds the read buffer.
client down saturation When an unopened frame fills the client’s BUF_SIZE concepts/src/client.rs The same is_saturated() check on the client runtime’s down buffer. Error text: upstream frame larger than the client runtime's buffer. On the sending side, make_room fails with frame larger than the client runtime's buffer when a packet plus the reserve cannot fit even in an empty staging buffer.
WORK_BUDGET 64 units of work concepts/src/runtime.rs Future::poll of a quiet runtime calls poll_once at most 64 times, then wakes itself and returns Pending, so a busy connection cannot monopolise a worker. The progress mode (showing_progress) is a Stream that yields after every unit instead. In the app, the Hysteria 2 stream and datagram runtimes and the TUN UDP associations are awaited as quiet futures; drive and the TUN inbound’s serve_stream use progress mode.
INLINE_EFFECTS 4 concepts/src/core.rs Inline capacity of EffectList, a SmallVec. A longer effect list spills to the heap. Sized for Open + Forward + SetDeadline.
HANDSHAKE_TIMEOUT 10 s protocols/src/core/mod.rs Timing in every core. The app’s drive, the TUN inbound’s serve_stream and the SOCKS driver use the same constant for their own watchdogs.
RELAY_IDLE_TIMEOUT 300 s protocols/src/core/mod.rs Timing::touch re-arms it on every byte event in Relay and Closing. On expiry Timing::expired pushes Effect::Finish. The SOCKS driver reuses it; see Protocol frame caps and drivers.
SNIFF_TIMEOUT 300 ms protocols/src/sniff/mod.rs Timing::enter(Phase::Sniff, …). On expiry the core opens the flow with what it has collected.
SNIFF_LIMIT 4 KiB, across frames protocols/src/sniff/mod.rs Collector::push returns Verdict::Exhausted once 4 KiB are held; Collector::remaining never offers more.
MAX_DOMAIN_LEN 253 protocols/src/sniff/mod.rs plausible_domain rejects a longer sniffed host.
HARNESS_STAGING 64 KiB protocols/src/core/harness.rs The staging room CoreHarness offers a core per call. The harness drives a core by hand for tests and does no I/O; no production path uses it.
BidirectionalConnection BUF_SIZE 8192 per direction by default concepts/src/relay.rs One Box<[u8; BUF_SIZE]> per direction. The SOCKS driver instantiates it with RELAY_BUF.

Each opened outbound also costs one Arc<KeyWaker> (concepts/src/wake.rs), the connector’s future while it dials, and one BTreeMap entry. The runtime itself puts no cap on the number of outbounds. A core that multiplexes bounds them; see Mux.

Constant Value Defined in Enforced by
TRANSPORT_HANDSHAKE_TIMEOUT 10 s protocols/src/transports/accept.rs within() wraps one step of InboundTransport::accept: the TLS accept for tls, or, for ws and grpc, the optional TLS accept and the WebSocket upgrade or HTTP/2 handshake together, under one 10 s budget. Error text: tls handshake timed out, websocket handshake timed out, grpc handshake timed out; a WebSocket or gRPC transport over TLS reports its own name even when TLS was the slow part. serve_socket logs the failure at debug as inbound transport failed: <error>. Plain TCP and Unix sockets have no transport step.
TCP_KEEPALIVE_IDLE 120 s protocols/src/transports/keepalive.rs set_keepalive, applied to every TCP socket InboundTransport::accept receives and every socket TransportConnector::dial opens. Best effort: when the platform rejects the option, it logs could not enable TCP keepalive at debug and keeps the connection.
TCP_KEEPALIVE_INTERVAL 30 s same same
TCP_KEEPALIVE_RETRIES 3 same same
DEFAULT_CONNECT_TIMEOUT 10 s per address environment/src/dial/tcp.rs TcpDialer::connect wraps socket.connect in tokio::time::timeout: connect to <addr> timed out. connect_any tries addresses one after another (not Happy Eyeballs), so the worst case is 10 s times the number of addresses; when all fail it returns failed to connect to any address (…) with every per-address error. Used by the freedom outbound and by TransportConnector. with_connect_timeout overrides the value; nothing in the kernel or the app calls it.
SocketOptions::tcp_keepalive None by default environment/src/dial/socket.rs When set with with_tcp_keepalive, TcpDialer::socket sets the keepalive idle time. Nothing in the kernel or the app sets it. The app’s freedom outbound builds its dialer from SocketOptions::default() and does not call set_keepalive, so its sockets keep the platform’s keepalive default.
MAX_EARLY_DATA 16 KiB protocols/src/transports/ws/endpoint.rs Inbound: decode_early_data_header refuses a larger decoded Sec-WebSocket-Protocol payload, and the upgrade is answered 413 Payload Too Large. Outbound: an ?ed=N limit is clamped to N.min(MAX_EARLY_DATA).
MAX_WS_MESSAGE_LEN 1 MiB protocols/src/transports/ws/endpoint.rs ws_config() sets both max_message_size and max_frame_size, replacing tungstenite’s defaults of 64 MiB and 16 MiB, on every session, accepted or dialed.
WS_KEEPALIVE_INTERVAL 60 s protocols/src/transports/ws/stream.rs The read side of WsStream queues a Ping when this timer fires. touch() resets it on every received frame and every write.
WS_IDLE_TIMEOUT 300 s protocols/src/transports/ws/stream.rs The read side reports end of stream when 300 s pass with no received frame and no write. Any frame counts, Pong included, which is what makes the Ping a liveness probe.
H2_INITIAL_STREAM_WINDOW_SIZE 4 MiB protocols/src/transports/grpc/settings.rs configured_server_builder and configured_client_builder (HTTP/2 flow control).
H2_INITIAL_CONNECTION_WINDOW_SIZE 16 MiB same same
H2_MAX_FRAME_SIZE 256 KiB same same
H2_MAX_CONCURRENT_STREAMS 256 same Server builder only: at most 256 gRPC tunnel streams per HTTP/2 connection.
MAX_GRPC_MESSAGE_LEN 1 MiB same The Hunk decoder in grpc/framing.rs checks the declared length before it buffers the body: grpc message length exceeds the accepted maximum.
H2_IDLE_TIMEOUT 300 s same Liveness ends a served connection that carries no streams for 300 s. serve_h2 calls note_progress on every accepted stream and on every stream-count change while streams are open, which restarts the deadline.
H2_KEEPALIVE_INTERVAL 60 s same Liveness sends a PING every 60 s on a served connection.
H2_KEEPALIVE_TIMEOUT 20 s same A PING that is not answered within 20 s ends the served connection.

Admission limits decide whether a new connection, stream or flow is taken at all. Apart from the QUIC stream credit (DEFAULT_MAX_INCOMING_STREAMS), none of them queue: each uses try_acquire_owned or try_send, and a refusal is immediate.

Constant Value Defined in Enforced by
MAX_LIVE_CONNECTIONS_PER_INBOUND 65,536 app/src/serve.rs A Semaphore per stream inbound. run_stream_inbound takes one permit per accepted socket, and serve_connection holds it (as _session) until the socket and every stream it yields are done. At the limit the socket is dropped and the loop logs dropping inbound connection; live connection limit reached at debug.
MAX_HANDSHAKES_PER_INBOUND 2048 app/src/serve.rs A second Semaphore per stream inbound, taken after the live permit. drive drops its handshake permit (handshake.take()) once the core reports is_established(). At the limit: dropping inbound connection; handshake limit reached at debug.
ACCEPT_ERROR_BACKOFF 100 ms app/src/serve.rs After an accept error other than ConnectionAborted or Interrupted (for example EMFILE), should_backoff_accept_error is true, the loop logs accept error, backing off 100ms at warn, and sleeps 100 ms or until cancelled.
DEFAULT_MAX_CONNECTIONS 4096 protocols/src/hysteria/server/config.rs Hysteria 2 listener: one permit per QUIC connection, taken before the handshake completes and held for the connection’s life. At the limit incoming.refuse() tells the client at once. Config key max_connections.
DEFAULT_MAX_CIRCUITS 65,536 protocols/src/hysteria/server/config.rs ServerConfig::circuit_permits, shared by every connection on the listener. A proxy stream takes a permit before its runtime is spawned and moves it into the task. A UDP session takes one when it opens and keeps it in its Session. At the limit a stream is reset with H3_REQUEST_REJECTED, and a datagram is dropped. Config key max_circuits.
DEFAULT_MAX_INCOMING_STREAMS 1024 protocols/src/hysteria/server/endpoint.rs max_concurrent_bidi_streams in the QUIC transport config. QUIC withholds stream credit, so the client’s open_bi waits rather than being refused.
MAX_CLASSIFYING_STREAMS 64 protocols/src/hysteria/server/inbound.rs Capacity of the two mpsc channels between the HTTP/3 layer and the stream classifier of one connection. When the channel toward the classifier is full, Hy2H3Conn::poll_accept_bidi (protocols/src/hysteria/server/shim.rs) resets the new stream with H3_EXCESSIVE_LOAD and logs hysteria2: resetting a stream; too many are awaiting classification at debug.
DEFAULT_MAX_FLOWS 65,536 protocols/src/tun/config.rs TUN: one Semaphore across the device. Each TCP stream and each UDP association (one per client source address) takes a permit. At the limit the flow is dropped: tun: dropping a flow; the flow limit is reached at debug. Config key max_flows.
FLOW_QUEUE 16 protocols/src/tun/udp.rs Capacity of the channel that hands new flows of one client source to its association. When it is full, try_send fails and the new flow is dropped; the client retransmits.

The mux.cool demultiplexer, Demux, is shared by the Trojan, VLESS and VMess cores.

Constant Value Defined in Enforced by
MAX_SESSIONS 256 sub-flows per carrier protocols/src/mux/demux.rs Demux answers a New frame with End and discards its payload when 256 sessions are open or the id is already live. The carrier stays up.
MAX_META_LEN 512 protocols/src/mux/frame.rs The frame parsers reject a longer metadata block: mux: metadata length … exceeds 512.
MAX_DATA_LEN 8 KiB protocols/src/mux/frame.rs The frame parsers reject a longer data block: mux: data length … exceeds 8192. On the downlink, Demux::on_outbound splits a read into Keep frames of at most 8 KiB, and Demux::on_datagram drops a packet longer than 8 KiB.
FRAME_OVERHEAD_MAX 268 protocols/src/mux/frame.rs 2 + MIN_META_LEN + 1 + AddressCodec::MAX_LEN + 2, with MIN_META_LEN = 4: the largest Keep header.
downlink_overhead(16 * 1024) 536 protocols/src/mux/demux.rs Two Keep headers. Part of the staging reserve of TrojanCore and VlessCore.

The app does not dial a UDP flow as a unit. AppConnector::connect returns a FanOutLink, which routes each datagram by its own destination and keeps one sub-link per chosen outbound.

Constant Value Defined in Enforced by
MAX_SUBS 64 sub-links per association app/src/outbound/udp_fanout.rs FanOutLink::poll_opening pushes the new sub-link and, when the table then holds more than 64, removes the front entry. A send moves its sub-link to the back, so the evicted one is the least recently sent to. Dropping it closes that outbound’s socket.

A sub-link that fails to open is logged at debug (udp fan-out: opening an outbound failed), and the packet that triggered it is not delivered.

Server side:

Constant Value Defined in Enforced by
STREAM_RECEIVE_WINDOW 8 MiB protocols/src/hysteria/server/endpoint.rs QUIC per-stream receive window.
CONNECTION_RECEIVE_WINDOW 20 MiB (STREAM_RECEIVE_WINDOW / 2 * 5) same QUIC per-connection receive window.
MAX_IDLE_TIMEOUT 30 s same QUIC max_idle_timeout: quinn closes a connection that is silent for 30 s.
CLASSIFY_TIMEOUT 10 s protocols/src/hysteria/server/inbound.rs A stream must reveal its frame type within 10 s, or it is reset with H3_REQUEST_CANCELLED.
DRAIN_TIMEOUT 3 s same Hy2Inbound::shutdown closes the endpoint with CLOSE_CODE, then waits up to 3 s for endpoint.wait_idle().
RELEASE_TIMEOUT, RELEASE_POLL 3 s, 20 ms same shutdown then tries to bind the port every 20 ms, for up to 3 s, so the next generation can bind it. A timeout logs hysteria2: <addr> did not come free within 3s at warn.
CLOSE_CODE 0x100 same QUIC application error code sent to every client on teardown.
MAX_SESSIONS 256 UDP sessions per connection protocols/src/hysteria/server/datagrams.rs While 256 sessions are open, Hy2UdpCore drops every packet that names a new session id (logged at trace).
SWEEP_INTERVAL 1 s same The core’s deadline. Each expiry advances its tick clock and closes sessions that have been quiet for udp_idle_timeout whole seconds (at least 1).
udp_idle_timeout 60 s default, 2 to 600 s accepted app/src/inbound/mod.rs build_hysteria2_inbound refuses values outside the range, and refuses the key when udp is off.

Protocol and client side:

Constant Value Defined in Enforced by
MAX_ADDRESS_LENGTH 2048 protocols/src/hysteria/protocol.rs Encoding and parsing of TCPRequest and UDPMessage addresses.
MAX_MESSAGE_LENGTH 2048 same The longest TCPResponse message that is encoded or parsed.
MAX_MESSAGE_KEPT 128 same How many characters of a server-supplied message survive, sanitised, into an io::Error on the client.
MAX_PADDING_LENGTH 4096 same The longest padding accepted on any frame.
MAX_UDP_SIZE 4096 same The Defragger refuses to reassemble beyond 4096 bytes. Also Hy2UdpCore::MAX_DATAGRAM.
MAX_DATAGRAM_FRAME_SIZE 1200 same The server’s datagram task passes it to Hy2UdpCore as the peer’s datagram size when the connection reports no max_datagram_size(); longer replies are fragmented.
MIN_PSK_LEN 4 bytes protocols/src/hysteria/obfs.rs Shortest Salamander key. The app refuses a shorter obfs_password on both sides: obfs_password must be at least 4 bytes for salamander.
CONNECT_TIMEOUT 10 s per address protocols/src/hysteria/connection.rs Hy2Conn::connect bounds each address attempt: the socket bind, the QUIC handshake and the /auth round trip. The server name is resolved once, before the attempts, outside this timeout. A timed-out attempt is recorded as <addr>: timed out, and when every address fails the error is hysteria2: no address answered (…).
OPEN_STREAM_TIMEOUT 5 s same Bounds open_bi when a proxy stream is opened: hysteria2: timed out opening a proxy stream.
DEFAULT_MAX_CONCURRENT_STREAMS 102,400 protocols/src/hysteria/config.rs The client’s streams semaphore. A permit is taken with try_acquire_owned before a stream is opened and held while it relays: hysteria2: connection is at its concurrent-stream limit. The default sits far above a default server’s 1024 streams, so in practice the server’s stream credit binds first and open_bi waits, up to OPEN_STREAM_TIMEOUT. Config key max_concurrent_streams; 0 is refused.
MAX_UDP_SESSIONS 256 protocols/src/hysteria/connection.rs hysteria2: connection is at its UDP association limit.
UDP_SESSION_BACKLOG 256 datagrams same Capacity of each association’s receive channel. When it is full, try_send fails and the datagram is dropped, so a slow consumer never stalls the connection’s reader.
KEEP_ALIVE 10 s same QUIC keep_alive_interval on the client. The client’s windows and MAX_IDLE_TIMEOUT match the server’s.
RECONNECT_BACKOFF_BASE, RECONNECT_BACKOFF_MAX 1 s, 30 s protocols/src/hysteria/slot.rs ConnSlot::note_failure sets retry_at to BASE * 2^min(failures, 5), capped at MAX: 2 s, 4 s, 8 s, 16 s, then 30 s.
MIN_HEALTHY_LIFETIME 10 s same A connection lost within 10 s of starting counts as a failure (died_young), so backoff accumulates against a server that authenticates and then closes.
Constant Value Defined in Enforced by
DEFAULT_MTU 1420 protocols/src/wireguard/config.rs InMemoryDevice reports it as smoltcp’s max_transmission_unit. Config key mtu.
CHANNEL_CAP 256 items protocols/src/wireguard/device.rs Capacity of the device’s command channel and of the two mpsc channels between each tunnel flow and the application side. The channels count items, not bytes: one WgStream write is one item of the caller’s buffer size. The driver holds at most one uplink item per connection (Uplink) and reads that connection’s channel only while it holds none, so the most one connection can queue on its way up is its 64 KiB TCP_BUFFER send ring, plus 256 channel items, plus 1 held item; past that, the writer waits. CHANNEL_CAP is also the most items the driver hands one connection’s socket per pass, so no producer can keep the driver to itself. On the way down, the driver reads a socket only while that connection’s channel has room.
TCP_BUFFER 64 KiB same Size of each smoltcp TCP ring buffer (one per direction), and of the payload store of each UDP packet buffer (one per direction, with 64 metadata slots). A UDP datagram that finds the send ring full is held until the next poll empties it. A datagram larger than the whole 64 KiB send ring can never be sent, so the driver drops it (logged at trace) instead of retrying it, and later datagrams keep flowing.
SCRATCH 64 KiB same The driver’s encapsulation scratch buffer and its UDP receive buffer, once per device.
TIMER_TICK 250 ms same tokio::time::interval that drives Tunn::update_timers.
EPHEMERAL_BASE 49152 same First tunnel-side source port; ports wrap from 65535 back to 49152.
smoltcp poll delay 2 ms when a repoll is pending, else smoltcp’s own delay, else 3600 s compute_sleep in the same file Inline literals, not named constants.
TCP_CONNECT_ATTEMPT_TIMEOUT 10 s per address protocols/src/wireguard/slot.rs connect_tcp_any bounds each in-tunnel TCP connect.
REBUILD_BACKOFF_BASE, REBUILD_BACKOFF_MAX 1 s, 30 s same DeviceSlot::note_failure, with the same arithmetic as the Hysteria slot. While the slot is backing off, acquire_device fails with wireguard: tunnel is down, waiting before the next attempt and builds no tunnel.
MIN_HEALTHY_LIFETIME 10 s same A device that dies within 10 s counts as a failed start, because WgDevice::start performs no handshake and succeeds even against an unreachable peer.
Constant Value Defined in Enforced by
DEFAULT_MTU 1500 protocols/src/tun/config.rs Passed to the device and to IpStackConfig::mtu. app/src/inbound/tun.rs refuses a configured mtu below 1280: tun mtu must be at least 1280.
DEFAULT_UDP_IDLE_TIMEOUT 60 s same IpStackConfig::udp_timeout: the stack ends a quiet (source, destination) UDP flow. Config key udp_idle_timeout.
DEFAULT_MAX_FLOWS 65,536 same See Admission.
FLOW_QUEUE 16 protocols/src/tun/udp.rs See Admission.
RELEASE_TIMEOUT, RELEASE_POLL 3 s, 20 ms protocols/src/tun/inbound.rs TunInbound::shutdown polls every 20 ms, for up to 3 s, until the device descriptor and every tracked TCP flow are released. A timeout logs a warn.

Two timers apply to TUN UDP. The stack’s udp_timeout ends each (source, destination) flow, and the association’s TunUdpCore runs Timing, so a whole association that is quiet for RELAY_IDLE_TIMEOUT finishes.

A TCP flow in the TUN inbound dials its outbound only after the client’s first bytes. serve_stream therefore wraps each runtime step in HANDSHAKE_TIMEOUT until the PassthroughCore is established, and a server-speaks-first protocol ends with tun: the client never spoke after 10 s.

Constant Value Defined in Enforced by
CACHE_CAPACITY 8192 names protocols/src/dns/mod.rs moka::future::Cache::max_capacity. The key is a name a client chooses, so the cache must be bounded.
SYSTEM_TTL 60 s same TTL for Backend::System answers, because getaddrinfo reports none.
MIN_TTL, MAX_TTL 5 s, 3600 s same resolve clamps every TTL into this range before it caches the answer. MAX_TTL is also moka’s cache-wide time_to_live. with_ttl_bounds overrides the range; the app does not call it.
QUERY_TIMEOUT 5 s same Wraps each query of the Udp, Tls and Https backends: dns: query timed out. The A and AAAA queries run concurrently, each with its own timeout. Backend::System goes through tokio::net::lookup_host and is not wrapped.
UDP_BUF 512 bytes same Receive buffer of the plain UDP backend. No EDNS is requested.
MAX_POINTER_HOPS 16 protocols/src/dns/message.rs skip_name steps over at most 16 labels of a name before it gives up (Truncated("dns name pointer chain")). It never follows a compression pointer (a pointer ends the name in place), so a pointer cycle cannot loop.
MAX_NAME_LEN 255 same Longest name encode_query accepts: dns: name too long (<n> bytes).
Constant Value Defined in Enforced by
DEFAULT_PROBE_INTERVAL 30 s app/src/balancer.rs spawn_probe runs one task per member. It probes at once, then sleeps this long after each probe, so the period is the probe’s duration plus the interval. Config key probe_interval (seconds).
DEFAULT_PROBE_TIMEOUT 5 s same A probe resolves the member’s upstream and tries a TCP connect to each address in turn, all inside one tokio::time::timeout. Config key probe_timeout (seconds).

Members start healthy. A change of state is logged at info as balancer member <tag> is now up or down. When every member is down, Balancer::select falls back to the first member instead of failing the flow. Both keys accept 0: probe_timeout = 0 leaves a probe no time to connect, so members are marked down and traffic falls back to the first member, and probe_interval = 0 probes back to back.

These bound what a parser accepts, what an encoder produces, or what a protocol driver buffers. A parser cap fails the frame where it is parsed. A frame with no parser cap is bounded only by the buffer it has to fit in, and ends the connection with FrameTooLarge when it does not.

Constant Value Defined in Meaning
MAX_HEAD 64 KiB protocols/src/http/protocol.rs Longest HTTP request head, and HttpCore::BUF_SIZE. HttpCore fails with http head exceeds maximum size once the unparsed bytes reach it without a complete head. HttpConnect::reply checks the same bound on the proxy’s response head (http: proxy response head exceeds maximum size), but in the app its client runtime of HTTP_BUF (16 KiB) saturates first.
MAX_HEADERS 128 same httparse header slots per head.
HttpConnect::REQUEST_MAX 1024 protocols/src/http/codec.rs Upper bound on the CONNECT request the HTTP client stages, and its STAGING_RESERVE.
AddressCodec::MAX_LEN 259 protocols/src/helpers/address.rs 1 + 1 + 255 + 2: type, length, domain, port. Part of every reserve that includes an address.
RELAY_BUF 16 KiB protocols/src/socks/server.rs Buffer per direction of a SOCKS CONNECT relay (BidirectionalConnection<A, B, RELAY_BUF>). Every RELAY_IDLE_TIMEOUT, relay_with_idle_guard compares the relayed byte counts with the previous check, and ends the relay with the error socks: relay idle when nothing moved. An idle relay therefore ends between 300 s and 600 s after its last byte.
HUB_BUF 64 KiB same Each of the two packet buffers of a SOCKS UDP ASSOCIATE relay. The association resets its RELAY_IDLE_TIMEOUT timer on every packet it forwards in either direction and ends when the timer fires. ExpectedSender::admits checks each datagram’s sender before the packet is parsed, and a datagram from an IP other than the client’s, or from another port once the client’s port is pinned, is discarded without resetting the timer, so traffic from another address cannot hold an association open. See The association’s client.
RECV_BUF 64 KiB protocols/src/socks/udp_link.rs Receive buffer of SocksUdpLink, the SOCKS client’s UDP association. Every datagram is read into it, and one whose sender is not the relay, compared with endpoint (protocols/src/socks/protocol.rs), is discarded before it is parsed.
MAX_LENGTH 8192 protocols/src/trojan/protocol.rs Largest Trojan UDP packet payload, and TrojanCore::MAX_DATAGRAM.
MAX_PAYLOAD (VMess) 1966 protocols/src/vmess/framing.rs 2048 - TAG_SIZE - 2 - 64: the most plaintext VMessCore and the VMess codecs put in one body chunk they seal, so a padded chunk never exceeds 2 KiB.
MAX_PADDING, CHUNK_OVERHEAD_MAX 64, 82 same Longest global-padding run on a chunk, and the most one sealed chunk adds beyond its plaintext (2 + TAG_SIZE + MAX_PADDING).
AUTHID_WINDOW_SECS 120 s protocols/src/vmess/accounts.rs An auth id is accepted while its timestamp is within 120 s of the server clock, in either direction.
REPLAY_TTL 240 s same 2 * AUTHID_WINDOW_SECS: how long a seen auth id is remembered, across REPLAY_SHARDS (64) shards.
MAX_PAYLOAD (Shadowsocks) 0x3FFF protocols/src/ss_legacy/aead.rs Largest AEAD chunk payload this side seals. CHUNK_OVERHEAD is 34 (2 + 16 + 16). The chunk decoder does not check a received length against it; a received chunk only has to fit in the 20 KiB buffer (ShadowsocksCore::BUF_SIZE, SS_BUF), or the connection ends with FrameTooLarge.
MAX_PADDING_LENGTH 900 protocols/src/ss_2022/protocol.rs The Shadowsocks 2022 client adds 1 to 900 random padding bytes to a request whose initial payload is shorter than 900 bytes.
MAX_PACKET_SIZE 0xFFFF protocols/src/ss_2022/crypto.rs Largest Shadowsocks 2022 record payload. The server core and the client codec split what they seal into records of at most this size. A received record is opened in place, so it has to fit in the 32 KiB buffer; see Server cores.
Constant Value Defined in Enforced by
reload debounce 200 ms app/src/main.rs (inline literal in spawn_watcher) The config watcher drains queued file events, sleeps 200 ms and drains again before it calls reload() once.
HTTP_BUF, SOCKS_BUF, TROJAN_BUF, VLESS_BUF 16 KiB app/src/outbound/mod.rs Client runtime buffer sizes; see Client codecs.
VMESS_BUF, SS2022_BUF 32 KiB same same
SS_BUF 20 KiB same same

Only these limits are read from the configuration file. Everything else on this page is compiled in.

Config key Struct in app/src/config.rs Default when absent Validation in the app
max_connections Hysteria2InboundSettings DEFAULT_MAX_CONNECTIONS (4096) 0 is refused: max_connections must be at least 1
max_circuits Hysteria2InboundSettings DEFAULT_MAX_CIRCUITS (65,536) 0 is refused: max_circuits must be at least 1
udp_idle_timeout Hysteria2InboundSettings 60 s 2 to 600 s: udp_idle_timeout must be between 2 and 600 seconds. Refused when udp is off: udp_idle_timeout is set but udp is not enabled
max_concurrent_streams Hysteria2OutboundSettings DEFAULT_MAX_CONCURRENT_STREAMS (102,400) 0 is refused: max_concurrent_streams must be at least 1. No upper bound.
mtu TunInboundSettings DEFAULT_MTU (1500) At least 1280
udp_idle_timeout TunInboundSettings DEFAULT_UDP_IDLE_TIMEOUT (60 s) None
max_flows TunInboundSettings DEFAULT_MAX_FLOWS (65,536) None. 0 is accepted and admits no flow.
obfs_password Hysteria2InboundSettings, Hysteria2OutboundSettings None At least MIN_PSK_LEN (4) bytes when obfs = "salamander": obfs_password must be at least 4 bytes for salamander
mtu WireguardOutboundSettings DEFAULT_MTU (1420) None
keepalive WireguardOutboundSettings None (no persistent keepalive) A u16 number of seconds, passed to boringtun’s Tunn::new as the persistent keepalive interval
ed=N in path WsStreamConfig, on an outbound No early data Parsed from the path’s query by WsTarget::new; the limit is clamped to MAX_EARLY_DATA (16 KiB), and 0 disables early data
probe_interval, probe_timeout BalancerConfig DEFAULT_PROBE_INTERVAL (30 s), DEFAULT_PROBE_TIMEOUT (5 s) None; 0 is accepted for both (see Balancer)

The app prefixes each message with its context. For an inbound tagged h, etemenanki-app --test -c on a config with max_connections = 0 logs an error line ending in configuration invalid: inbound h: max_connections must be at least 1 and exits with a failure status.

The runtimes allocate their byte buffers once, at construction, and never grow them. The fixed userspace cost of a connection is therefore a sum of constants:

  • A server runtime (ProxyServerRuntime) allocates three boxed arrays of BUF_SIZE: up (transport reads), staging (bytes toward the transport) and scratch (outbound reads).
  • Every stream core also owns a SniffPrefix, whose Collector::new calls Vec::with_capacity(SNIFF_LIMIT). That is 4 KiB per connection, allocated when the core is built, whether or not sniffing is on. SniffPrefix::clear replaces the collector with a fresh Collector::new(), so the 4 KiB stays allocated for the life of the connection. TunUdpCore and Hy2UdpCore have no SniffPrefix.
  • A client runtime (ProxyClientRuntime) allocates two boxed arrays of its BUF_SIZE: staging (sealed bytes toward the upstream) and down (wire bytes from it). It also holds the boxed dial future until the dial completes.

A connection routed to a proxy outbound pays for both runtimes. A connection routed to freedom pays only for the server side, because the outbound is a kernel socket.

Core Runs BUF_SIZE Sized for STAGING_RESERVE MAX_DATAGRAM datagram_limit() 3 × BUF_SIZE With SniffPrefix
HttpCore one HTTP inbound connection 64 KiB (MAX_HEAD) a whole request head 256 4096 (default) stream only 192 KiB 196 KiB
TrojanCore one Trojan inbound connection 16 KiB a UDP frame: header plus 8 KiB 808 8192 (MAX_LENGTH) 8192 48 KiB 52 KiB
VlessCore one VLESS inbound connection 16 KiB a UDP frame of MAX_DATAGRAM behind its length 808 8192 8192 48 KiB 52 KiB
VMessCore one VMess inbound connection 32 KiB not stated in the source 4096 8192 8192 96 KiB 100 KiB
ShadowsocksCore one Shadowsocks inbound connection 20 KiB one MAX_PAYLOAD chunk plus overhead 128 4096 (default) stream only 60 KiB 64 KiB
Ss2022Core one Shadowsocks 2022 inbound connection 32 KiB a padded request chunk, or a record 256 4096 (default) stream only 96 KiB 100 KiB
Hy2StreamCore one Hysteria 2 proxy stream 8 KiB a request with the longest address and padding 2048 4096 (default) stream only 24 KiB 28 KiB
Hy2UdpCore the datagrams of one Hysteria 2 connection 16 KiB a reassembled packet plus fragment headers 4096 4096 (MAX_UDP_SIZE) 4096 48 KiB 48 KiB (no prefix)
PassthroughCore one TUN TCP flow 8 KiB “every buffer size this core needs” 0 4096 (default) stream only 24 KiB 28 KiB
TunUdpCore one TUN UDP association (one client source) 8 KiB a datagram plus the reserve 4096 4096 4096 24 KiB 24 KiB (no prefix)

How the non-literal reserves add up:

  • Trojan: PACKET_HEADER_MAX.next_multiple_of(16) + downlink_overhead(Self::BUF_SIZE). PACKET_HEADER_MAX is 259 + 2 + 2 = 263, rounded up to 272, and downlink_overhead(16 * 1024) is 2 * 268 = 536, so the total is 808.
  • VLESS: 272 + downlink_overhead(Self::BUF_SIZE), also 808.
  • Shadowsocks: 32 + 2 * CHUNK_OVERHEAD + 28, with CHUNK_OVERHEAD = 34, is 128.
  • Shadowsocks 2022: 32 + 1 + 8 + 32 + 2 + 2 * TAG_SIZE + RECORD_OVERHEAD + 115, with TAG_SIZE = 16 and RECORD_OVERHEAD = 34, is 256.

A core that opens frames in place needs each frame to fit in BUF_SIZE, and a larger frame ends the connection with FrameTooLarge. For Ss2022Core that means a record payload of no more than 32,734 bytes (32 KiB less the 34-byte RECORD_OVERHEAD) once the session is running, although the protocol allows up to 0xFFFF. For ShadowsocksCore it means a chunk payload of no more than 20,446 bytes (20 KiB less the 34-byte CHUNK_OVERHEAD).

The Hysteria 2 listener costs 28 KiB per proxy stream, plus 48 KiB per connection when udp is enabled; the datagram runtime starts only after the connection authenticates.

The SOCKS inbound is the one stream protocol that does not run a core, so it has its own budget:

SOCKS path Buffers Total
CONNECT BidirectionalConnection<A, B, RELAY_BUF>: one 16 KiB buffer per direction, allocated when the relay starts. With sniffing on and an IP target, collect_prefix first holds a 4 KiB read chunk and a Collector with 4 KiB of capacity (8 KiB before the relay exists). The collected prefix stays in scope in connect for the rest of the relay. 32 KiB, or 36 KiB after a sniff
UDP ASSOCIATE up and down, each HUB_BUF (64 KiB), a 2 KiB packet BytesMut and a 256-byte sink array. associate allocates them only after ExpectedSender::new has accepted the request and the relay socket is bound, so an association refused with reply 0x02 costs none of them. about 130 KiB
Outbound Codec Buffer constant STAGING_RESERVE 2 × BUF
HTTP HttpConnect HTTP_BUF = 16 KiB 1024 (REQUEST_MAX) 32 KiB
SOCKS (CONNECT) SocksConnect SOCKS_BUF = 16 KiB 528 (a 513-byte credential message, or a request with a full address) 32 KiB
Trojan TrojanStream, TrojanDatagram TROJAN_BUF = 16 KiB 320 (REQUEST_HEADER_MAX = 56 + 2 + 1 + 259 + 2) 32 KiB
VLESS VlessStream, VlessDatagram VLESS_BUF = 16 KiB 278 (REQUEST_HEADER_MAX = 1 + 16 + 1 + 1 + 259) 32 KiB
VMess VMessStream, VMessDatagram VMESS_BUF = 32 KiB 384 (HEADER_MAX = 358, rounded up to a multiple of 64) 64 KiB
Shadowsocks SsStream SS_BUF = 20 KiB 386 (32 + CHUNK_OVERHEAD + AddressCodec::MAX_LEN + 61) 40 KiB
Shadowsocks 2022 Ss2022Stream SS2022_BUF = 32 KiB 2048 64 KiB

Outbounds that do not use a client runtime:

Outbound Per-flow userspace buffers
freedom None; the outbound is a kernel socket.
blackhole None.
SOCKS UDP ASSOCIATE SocksUdpLink: a 64 KiB RECV_BUF array and a 2 KiB scratch BytesMut, about 66 KiB.
WireGuard, TCP flow Two smoltcp ring buffers of TCP_BUFFER: 128 KiB. Channel items come on top of the two rings, and they are bounded: up to CHANNEL_CAP (256) plus one held item on the way up, and up to 256 on the way down, each as large as the write or socket read that produced it. The device adds 2 × SCRATCH (128 KiB) once, shared by all its flows.
WireGuard, UDP flow Two smoltcp packet buffers with TCP_BUFFER of payload each: 128 KiB. Channel items come on top, bounded the same way: up to CHANNEL_CAP (256) datagrams plus one held on the way up, and up to 256 on the way down.
Hysteria 2 No fixed buffer. QUIC flow control bounds data in flight at STREAM_RECEIVE_WINDOW per stream and CONNECTION_RECEIVE_WINDOW per connection.
Inbound → outbound Server side Client side Approximate total
Trojan → freedom 52 KiB 0 52 KiB
VLESS → VLESS 52 KiB 32 KiB 84 KiB
VMess → VMess 100 KiB 64 KiB 164 KiB
HTTP → freedom 196 KiB 0 196 KiB
Shadowsocks 2022 → WireGuard (TCP) 100 KiB 128 KiB 228 KiB
SOCKS CONNECT → Trojan 32 KiB 32 KiB 64 KiB
TUN TCP → freedom 28 KiB 0 28 KiB

A mux.cool carrier pays for its server runtime once. Each sub-flow routed to a proxy outbound adds its own client runtime. A Trojan carrier with the maximum 256 sub-flows through a VLESS outbound therefore holds 52 KiB + 256 × 32 KiB = 8,244 KiB, about 8 MiB.

Invariant Mechanism Pinned by
A runtime can always stage its reserve assert!(BUF_SIZE > Core::STAGING_RESERVE) in ProxyServerRuntime::build, assert!(BUF_SIZE > Codec::STAGING_RESERVE) in ProxyClientRuntime::new Every runtime built in the tests, at each core’s own BUF_SIZE; there is no dedicated should_panic test
A frame larger than the read buffer is an error, not a stall poll_transport_read returns FrameTooLarge when up.is_saturated() frame_larger_than_the_buffer_is_an_error in concepts/tests/runtime.rs
Staging backpressure never truncates a datagram from an outbound A datagram outbound is polled only with STAGING_RESERVE + datagram_limit() of room datagram_outbound_is_never_truncated_by_staging_backpressure in concepts/tests/runtime.rs
A stalled outbound stalls its own uplink, not other downlinks Effects apply in order, and a blocked forward pins the transport read stalled_outbound_holds_uplink_but_not_other_downlink in concepts/tests/runtime.rs
The client runtime never buffers beyond its two arrays poll_write waits for STAGING_RESERVE + 1 bytes of room backpressure_from_the_wire_reaches_the_writer in concepts/tests/client.rs
The handshake deadline is armed once, the idle deadline on every byte event Timing::touch timing_arms_handshake_once_then_idle_per_byte_event in protocols/tests/unit/core/mod.rs
A deadline fires against the tokio clock The runtime’s timer is a tokio Sleep, so a paused test clock advances it deadline_event_lets_the_core_time_out, deadline_is_armed_against_the_tokio_clock in concepts/tests/runtime.rs
Sniffing never takes more than SNIFF_LIMIT SniffPrefix::push takes at most Collector::remaining() sniff_prefix_takes_no_more_than_its_budget in protocols/tests/unit/core/mod.rs
A mux carrier never holds more than 256 sub-flows Demux declines New with End unknown_or_excess_sessions_are_declined_with_end in protocols/tests/unit/mux/demux.rs
gRPC and WebSocket never buffer a message beyond 1 MiB The MAX_GRPC_MESSAGE_LEN check runs before buffering; ws_config() decoder_rejects_an_oversized_length_before_buffering_the_body in grpc_framing.rs; the_configured_limits_replace_tungstenite_defaults in ws_endpoint.rs
A dial tries every address before failing TcpDialer::connect_any connect_any_falls_through_a_dead_address in environment/tests/integration/tcp.rs
The DNS cache stays bounded under distinct names moka max_capacity(CACHE_CAPACITY) the_cache_stays_bounded_under_distinct_names in protocols/tests/unit/dns/mod.rs
Reconnect backoff grows and is capped note_failure shifts by at most 5, then takes min(MAX) backoff_grows_with_each_failure_and_is_capped (Hysteria), repeated_failures_escalate_the_backoff (WireGuard)
A SOCKS UDP association’s buffers and idle timer serve only its client ExpectedSender::admits runs before parse_udp_packet and before the idle timer is reset: every datagram must come from the control connection’s IP, and the first one forwarded pins the port unless the request already named it with that IP. Over a Unix socket, the request must name the exact IP and port udp_association_ignores_another_ip (Linux only) and udp_association_ignores_another_port_once_pinned in protocols/tests/pipeline/socks.rs; only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port in protocols/tests/unit/socks/server.rs
Hysteria UDP sessions count against the circuit budget Hy2UdpCore takes a circuit_permits permit per session the_circuit_limit_refuses_new_sessions in protocols/tests/unit/hysteria/server/datagrams.rs
The WireGuard driver holds at most one uplink item per connection; a stalled tunnelled flow blocks its writer, not its siblings Uplink holds at most one item that the socket has not accepted, and poll_refill reads the channel only while nothing is held, so a full socket fills the bounded channel and the application’s sender waits a_stalled_tcp_flow_blocks_its_writer in protocols/tests/pipeline/wireguard.rs (a writer of 1 KiB chunks must wait before 512 KiB are in, against an expected 385 KiB, while another flow on the same tunnel still echoes); a_held_item_keeps_the_channel_unread in protocols/tests/unit/wireguard/device.rs

When a limit trips, the result is one of four kinds. Knowing the kind tells you where to look in the logs.

Kind Limits What the peer sees Log
Silent drop at admission MAX_LIVE_CONNECTIONS_PER_INBOUND, MAX_HANDSHAKES_PER_INBOUND, TUN max_flows and FLOW_QUEUE, Hysteria UDP MAX_SESSIONS, the circuit limit for Hysteria datagrams The socket closes, or the packet is lost debug or trace
Explicit refusal Hysteria max_connections (incoming.refuse()), the circuit limit on a Hysteria stream (H3_REQUEST_REJECTED), MAX_CLASSIFYING_STREAMS (H3_EXCESSIVE_LOAD), mux MAX_SESSIONS (an End frame), WebSocket early data (413) A protocol-level refusal; the carrier connection survives where there is one debug; the mux decline logs nothing
Connection error FrameTooLarge, HANDSHAKE_TIMEOUT, TRANSPORT_HANDSHAKE_TIMEOUT, MAX_GRPC_MESSAGE_LEN, MAX_WS_MESSAGE_LEN, the parser frame caps, the SOCKS relay idle check The connection closes debug. For a stream inbound, a transport failure is inbound transport failed: <error> from serve_socket, and a runtime or driver error is <protocol> connection from <source> ended: <error> from serve_connection, with the source printed as an Option
Clean end RELAY_IDLE_TIMEOUT in a core, WS_IDLE_TIMEOUT, H2_IDLE_TIMEOUT, the PING timeout, QUIC MAX_IDLE_TIMEOUT, the Hysteria UDP sweep, the SOCKS UDP ASSOCIATE idle timer The stream ends as if the peer had gone Usually none

Cancellation does not depend on any of these timers. Stream inbounds spawn every per-connection task with spawn_scoped under the generation’s CancellationToken. The Hysteria 2 and TUN inbounds own their per-connection work in a JoinSet inside their run future. Either way, ending a generation drops each runtime, and with it its buffers and permits. The Hysteria 2 and TUN inbounds then wait up to DRAIN_TIMEOUT and RELEASE_TIMEOUT for their socket or device to come free. See Generations and reload.

Test File Pins
frame_larger_than_the_buffer_is_an_error concepts/tests/runtime.rs FrameTooLarge
datagram_outbound_is_never_truncated_by_staging_backpressure concepts/tests/runtime.rs datagram_limit() and the datagram read condition
stalled_outbound_holds_uplink_but_not_other_downlink concepts/tests/runtime.rs Ordered effects as backpressure
deadline_event_lets_the_core_time_out, deadline_is_armed_against_the_tokio_clock concepts/tests/runtime.rs The single core deadline
backpressure_from_the_wire_reaches_the_writer concepts/tests/client.rs Client runtime staging bound
connect_any_falls_through_a_dead_address environment/tests/integration/tcp.rs Sequential per-address dialing
timing_arms_handshake_once_then_idle_per_byte_event, timing_reports_handshake_and_sniff_expiry_to_the_core protocols/tests/unit/core/mod.rs Timing
sniff_prefix_takes_no_more_than_its_budget, a_sniffing_passthrough_core_opens_on_the_sniff_deadline, passthrough_core_finishes_when_the_outbound_fails_or_idles protocols/tests/unit/core/mod.rs SNIFF_LIMIT, SNIFF_TIMEOUT, RELAY_IDLE_TIMEOUT
handshake_deadline_fails_the_connection, sniff_deadline_opens_with_what_was_collected protocols/tests/unit/trojan/core.rs Handshake and sniff deadlines in a real core
the_budget_ends_the_search_without_a_match protocols/tests/unit/sniff/collector.rs SNIFF_LIMIT
a_connection_that_opens_no_stream_is_given_up_on protocols/tests/unit/transports/accept.rs H2_IDLE_TIMEOUT, end to end
liveness_gives_up_on_a_connection_with_no_streams, liveness_keeps_a_connection_carrying_streams, liveness_restarts_the_idle_deadline_on_progress protocols/tests/unit/transports/grpc_liveness.rs Liveness
decoder_accepts_a_message_at_the_maximum, decoder_rejects_a_message_longer_than_the_maximum, decoder_rejects_an_oversized_length_before_buffering_the_body protocols/tests/unit/transports/grpc_framing.rs MAX_GRPC_MESSAGE_LEN
rejects_oversized_early_data_header, callback_rejects_oversized_early_data, the_configured_limits_replace_tungstenite_defaults protocols/tests/unit/transports/ws_endpoint.rs MAX_EARLY_DATA, MAX_WS_MESSAGE_LEN
unknown_or_excess_sessions_are_declined_with_end protocols/tests/unit/mux/demux.rs Mux MAX_SESSIONS
read_meta_enforces_the_length_caps protocols/tests/unit/mux/frame.rs MAX_META_LEN, MAX_DATA_LEN
an_oversized_head_is_refused protocols/tests/unit/http/core.rs MAX_HEAD
udp_association_ignores_another_ip, udp_association_ignores_another_port_once_pinned, udp_association_refuses_a_relay_that_cannot_hear_the_client, udp_link_ignores_datagrams_not_from_the_relay protocols/tests/pipeline/socks.rs Which datagrams the SOCKS UDP ASSOCIATE relay (HUB_BUF) and SocksUdpLink (RECV_BUF) act on, and the 0x02 refusal of an association whose client the relay could never hear
only_the_control_peer_is_heard_and_its_first_datagram_pins_the_port, a_relay_that_cannot_hear_the_client_is_refused protocols/tests/unit/socks/server.rs ExpectedSender and hears
rejects_oversize_udp_packet protocols/tests/unit/trojan/protocol.rs Trojan MAX_LENGTH
user_not_found_outside_time_window, replayed_authid_is_rejected, expired_ids_leave_the_set protocols/tests/unit/vmess/accounts.rs AUTHID_WINDOW_SECS, REPLAY_TTL
an_idle_session_is_closed_by_the_sweep, the_circuit_limit_refuses_new_sessions protocols/tests/unit/hysteria/server/datagrams.rs SWEEP_INTERVAL, circuit permits
reassembly_stops_at_the_maximum_datagram_size, a_payload_needing_more_than_255_fragments_is_refused, sanitise_message_caps_length_and_tolerates_bad_utf8 protocols/tests/unit/hysteria/protocol.rs MAX_UDP_SIZE, fragmentation, MAX_MESSAGE_KEPT
backoff_grows_with_each_failure_and_is_capped, a_connection_that_never_started_counts_as_dying_young, a_backing_off_slot_refuses_without_dialling protocols/tests/unit/hysteria/slot.rs Reconnect backoff, MIN_HEALTHY_LIFETIME
repeated_failures_escalate_the_backoff, a_short_lived_tunnel_counts_as_a_failure, the_backoff_gate_refuses_without_building_another_tunnel protocols/tests/unit/wireguard/slot.rs Rebuild backoff, MIN_HEALTHY_LIFETIME
a_held_item_keeps_the_channel_unread, the_uplink_finishes_only_after_its_last_item protocols/tests/unit/wireguard/device.rs Uplink: one held item, the channel left unread behind it, and the end of the uplink seen only after its last item is handed on
a_stalled_tcp_flow_blocks_its_writer, an_oversized_datagram_does_not_wedge_the_association protocols/tests/pipeline/wireguard.rs The CHANNEL_CAP bound on a stalled TCP flow; dropping a datagram larger than the TCP_BUFFER send ring
the_association_finishes_when_the_outbound_fails_or_idles protocols/tests/unit/tun/udp.rs TUN UDP association idle
the_cache_stays_bounded_under_distinct_names, an_expired_entry_is_looked_up_again protocols/tests/unit/dns/mod.rs CACHE_CAPACITY, TTL expiry
an_out_of_range_udp_idle_timeout_is_refused, a_udp_idle_timeout_without_udp_is_refused, a_zero_limit_is_refused, tun_refuses_an_mtu_below_the_stack_floor app/tests/unit/inbound.rs Config validation of limits
hysteria2_refuses_only_a_zero_stream_limit, the_default_stream_limit_is_configurable_as_well_as_defaulted app/tests/unit/outbound.rs max_concurrent_streams
accept_error_backoff_classification app/tests/unit/serve.rs Which accept errors back off
every_member_down_still_selects_rather_than_dropping app/tests/unit/balancer.rs Balancer fallback
traffic_moves_off_a_member_that_stops_answering app/tests/integration/e2e_balancer.rs Probe interval and timeout, end to end
one_association_routes_each_peer_separately, replies_from_several_peers_merge_back_correctly app/tests/integration/e2e_udp_route.rs The UDP fan-out

At this revision, these limits have no test that names them: WORK_BUDGET, MAX_SUBS eviction, MAX_LIVE_CONNECTIONS_PER_INBOUND, MAX_HANDSHAKES_PER_INBOUND, TRANSPORT_HANDSHAKE_TIMEOUT, the TCP keepalive schedule, WS_KEEPALIVE_INTERVAL and WS_IDLE_TIMEOUT, downlink_overhead, and the client-side Hysteria limits (OPEN_STREAM_TIMEOUT, MAX_UDP_SESSIONS, UDP_SESSION_BACKLOG).

  1. Find every value derived from it. Search for the constant’s name. Reserves are often computed from other constants (downlink_overhead(Self::BUF_SIZE), REQUEST_HEADER_MAX, AddressCodec::MAX_LEN), so raising a core’s BUF_SIZE can raise its own reserve.

  2. Keep the reserve below the buffer. BUF_SIZE must stay larger than STAGING_RESERVE, or the runtime panics when it is built. For a datagram core, also check that BUF_SIZE - STAGING_RESERVE is at least MAX_DATAGRAM. Otherwise datagram_limit() quietly shrinks and packets are truncated.

  3. Size a core’s BUF_SIZE to its largest whole frame. A frame the core must see whole (an HTTP head, a Shadowsocks chunk or record, a UDP frame behind its length) has to fit in BUF_SIZE, or the connection ends with FrameTooLarge. Size a client buffer to the largest wire frame its codec opens, plus the codec’s reserve.

  4. Update the memory budget. A server buffer costs three times the change, a client buffer twice. Update the tables on this page and, for a default an operator can see, the user guide.

  5. Pin it with a test. Refer to the constant by name in the test instead of repeating the literal, as the_cache_stays_bounded_under_distinct_names does with CACHE_CAPACITY. Then run the gates in Testing.