Limits & Backpressure
Know what each iroh lane admits under load, which signal a shed or stall leaves, and how to tune the bounds.
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 boundsAn 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.
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.
| Default | Value | Bounds |
|---|---|---|
DEFAULT_ACCEPT_STREAM_TIMEOUT | 30 s | accept_bi(), the peer opening its stream |
DEFAULT_READ_TIMEOUT | 30 s | Reading the one request frame |
DEFAULT_WRITE_TIMEOUT | 30 s | Writing the response frame |
DEFAULT_LINGER_TIMEOUT | 60 s | Post-exchange conn.closed() linger |
DEFAULT_MAX_IN_FLIGHT | 1024 | Concurrent accept handlers per lane |
DEFAULT_MAX_IN_FLIGHT_PER_PEER | 16 | Concurrent handlers per EndpointId |
DEFAULT_SHED_WAIT | 250 ms | Bounded 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 code | Constant | Sent for |
|---|---|---|
| 1 | LANE_RESET_CLOSE_CODE | A fail-closed lane error: framing, transport, or verification plumbing |
| 2 | ACCEPT_TIMEOUT_CLOSE_CODE | accept_timeout, a phase past its bound |
| 3 | ACCEPT_BUSY_CLOSE_CODE | accept_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.
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_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 # gaugeThe 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:
chio pheromone relay metrics --store ./relay.db --output ./relay-metrics.prom
grep -E 'lane_total|accept_open' ./relay-metrics.promchio_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"} 16Sixteen 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.