Chio/Docs
LOGIN · JOIN

Limits & Backpressure

Know what each iroh lane admits under load, which signal a shed or stall leaves, and how to tune the bounds.

Deployment chio@5f63e6e

On this page

Every connection into the signals, revocation, and co-sign lanes runs inside fixed bounds: at most 16 handlers per peer, 1024 per lane, and a timeout on each phase that waits on the peer. A slow or flooding peer is shed or reset with a close code that names the reason, and the other operators on the lane keep their share.

Those three lanes ride the optional iroh transport through self-hosted relays; cross-organization traffic travels over HTTPS with mutual TLS or signed bodies by default. The admission gate admits an endpoint first; the limits below apply to every connection it lets through.

Read the bounds
Accept boundsChoose an outcome.
admitted peersyour iroh laneaccept_bi30 sread_frame30 swrite_response30 slinger60 sthe handler holds its permits for this spane1peere2another peergBper-peer cap16 per peerlane permits1024 per lanewaits up to 250 msadmitted peersyour iroh laneaccept_bi30 sread_frame30 swrite_response30 slinger60 spermits held for this spane1peere2another peergBper-peer cap16 per peerlane permits1024 per lanewaits up to 250 ms

An admitted connection runs inside every bound

The per-peer cap admits the connection and the lane grants one of its 1024 permits. The handler then runs accept_bi, read_frame, and write_response under 30-second timeouts, and lingers up to 60 seconds for the dialer to close. It holds both permits until it ends. Transport Lanes.

One connection into an iroh lane after the admission gate, drawn with the default bounds; the phase spans are proportional to their timeouts. The lane-reset path for framing and verification failures, close code 1, is left out.

Every phase that waits on a peer has a bound

The three direct lanes share lanes::limits::AcceptLimiter, which bounds the accept path against slowloris and resource exhaustion before any frame reaches verification. AcceptLimitConfig carries a timeout for each AcceptPhase (AcceptStream, ReadFrame, WriteResponse), the linger, a lane-wide cap, a per-peer cap, and the shed wait.

DefaultValueBounds
DEFAULT_ACCEPT_STREAM_TIMEOUT30 saccept_bi(), the peer opening its stream
DEFAULT_READ_TIMEOUT30 sReading the one request frame
DEFAULT_WRITE_TIMEOUT30 sWriting the response frame
DEFAULT_LINGER_TIMEOUT60 sPost-exchange conn.closed() linger
DEFAULT_MAX_IN_FLIGHT1024Concurrent accept handlers per lane
DEFAULT_MAX_IN_FLIGHT_PER_PEER16Concurrent handlers per EndpointId
DEFAULT_SHED_WAIT250 msBounded wait for a permit before shedding

The read bound is the main slowloris defense: a peer that sends a large length prefix and withholds the body is dropped when read_frame passes 30 seconds. The linger lets the dialer read the response and close cleanly, and caps that wait so a peer that never closes cannot pin the handler.

A reset carries a close code that names its cause

A shed or a timeout closes the connection with a QUIC application code, so a stalled peer and an overloaded lane are told apart on the wire and never mistaken for a protocol reset.

Close codeConstantSent for
1LANE_RESET_CLOSE_CODEA fail-closed lane error: framing, transport, or verification plumbing
2ACCEPT_TIMEOUT_CLOSE_CODEaccept_timeout, a phase past its bound
3ACCEPT_BUSY_CLOSE_CODEaccept_busy and accept_peer_busy, a shed the peer retries later

A bound limits only how long a handler waits and how many peers it serves. The exchange either runs the per-frame verifier in full or resets with no verdict, and the metrics record the reset as a busy or timeout outcome for the lane.

Each lane handler takes its own limits

chio pheromone relay serve --iroh-enable mounts the lanes with the defaults. A host that embeds the transport passes an AcceptLimitConfig to each lane handler with with_accept_limits.

src/transport.rsrust
use chio_federation_transport_iroh::lanes::limits::AcceptLimitConfig;

// Start from the defaults and override only what this lane needs.
let limits = AcceptLimitConfig {
    max_in_flight_per_peer: 8,
    ..AcceptLimitConfig::default()
};
let handler = handler.with_accept_limits(limits);

The signals, revocation, and co-sign handlers (PheromoneBatchHandler, RevocationHandler, BilateralCoSignHandler) all accept the same config. Set the endpoint's QUIC idle timeout above the linger, 90 seconds by the adapter's recommendation, and allow one bidirectional stream per connection, since every direct lane uses exactly one.

One peer holds at most 16 of a lane's permits

The limiter reserves the per-peer slot before it waits on the lane's shared permits. A peer already at its cap sheds at once with accept_peer_busy (AcceptLimitError::PeerBusy) and close code 3, so a single admitted operator can never hold the permits another operator needs. A peer under its cap that finds the lane saturated waits up to 250 milliseconds, then sheds with accept_busy.

A reserved slot is released when its handler ends, when the lane sheds the connection, and when the accept is canceled mid-wait, so a dropped connection never leaves a peer capped until restart. A per-peer cap of 0 is clamped to 1, and a poisoned per-peer counter is treated as at-cap.

The transport counts every shed, stall, and lag

The slim iroh build drops iroh's own metrics feature, so the transport emits its own families through the workspace metrics registry. render_iroh_transport_metrics_prometheus returns a text block that the host's /metrics exporter concatenates.

chio_federation_transport_*text
chio_federation_transport_admission_total{outcome}          # accept | reject (probe signal)
chio_federation_transport_verify_failures_total{seam,reason}
chio_federation_transport_lane_total{lane,outcome}          # accept|reject|busy|timeout|lagged
chio_federation_transport_catchup_epoch_gap_total{source}
chio_federation_transport_outbox_total{outcome}             # delivered|retried|dead_lettered
chio_federation_transport_directory_reload_total{outcome}
chio_federation_transport_accept_open{lane}                 # gauge: in-flight accept handlers
chio_federation_transport_accept_duration_seconds{lane}     # histogram: accept-handler tail
chio_federation_transport_router_alive                      # gauge

The counters record events alongside the fail-closed logic and change no trust decision. Lane labels are the bounded vocabulary pheromone, revocation, bilateral, and fanout. Each accept also opens a tracing span through observability::lane_accept_span, which records the lane and the handler's outcome.

A lagged subscriber is counted as dropped messages

When the gossip subscriber falls behind, the fanout lane returns FanoutError::Lagged (wire name lagged) to the caller and counts it under lane_total with outcome lagged. A lagged event marks messages that were dropped; it is never an accepted frame. The gossip layer reports no drop count, so the counter records occurrences only.

Each symptom points at one bound

Read the verification reasons and the peer's activity before attributing a cause.

Admission rejects spike

A rise in admission_total{outcome="reject"} means endpoints outside the directory are dialing: a misconfigured partner, or something probing the mesh. Reload the directory if the peer should be admitted.

Open handlers climb and the duration tail lengthens

A climbing accept_open with a long accept_duration_seconds tail points to slow peers or a slow-trickle attack. The timeouts reset each stalled connection with close code 2, and lane_total counts it as timeout.

Busy outcomes climb on one lane

A climbing busy count means the lane cap or one peer's cap is saturated. Peers retry later and other peers are unaffected; raise the cap for that lane only after the load is confirmed legitimate. Export the relay's metrics and read the lane counters beside the open handlers:

Terminalbash
chio pheromone relay metrics --store ./relay.db --output ./relay-metrics.prom
grep -E 'lane_total|accept_open' ./relay-metrics.prom
Output
chio_federation_transport_lane_total{lane="pheromone",outcome="accept"} 12906
chio_federation_transport_lane_total{lane="pheromone",outcome="busy"} 0
chio_federation_transport_lane_total{lane="revocation",outcome="accept"} 1842
chio_federation_transport_lane_total{lane="revocation",outcome="busy"} 37
chio_federation_transport_accept_open{lane="pheromone"} 3
chio_federation_transport_accept_open{lane="revocation"} 16

Sixteen open revocation handlers is far below the lane's 1024, so the lane is not saturated: the 37 busy outcomes are a peer shedding at its own cap of 16, and the signals lane runs clear.

Source files

The limiter and its defaults are in crates/trust/chio-federation-transport-iroh/src/lanes/limits.rs, the lagged signal in lanes/fanout.rs, and the metric families and span in metrics.rs and observability.rs of the same crate.

Next: see what the lanes carry