Decision records
ADR-0038: Connection distribution — measure pressure, not connection counts
Status: accepted (design, E3) — 2026-10-07
Context
With two instances the busiest one holds 53–62 % of the connections, with the kernel hash
(reuseport) and with the shared queue (inherited) alike; long-lived connections stay
where they were accepted. Equal connection counts are not a goal: a keep-alive
connection that sends one request a minute costs nothing, an HTTP/2 connection with 100
streams costs a core.
Decision
-
Observe work, per instance: accepted connections per second, open HTTP/1, HTTP/2, WebSocket (and later QUIC) connections, requests per second, bytes per second, CPU (from
/proc/<pid>/stat), event-loop lag, in-flight requests, p95/p99. The controller already samples most of this for PressureScore (ADR-0034); it adds the per-instance view and an imbalance score:imbalance = max(pressure_i) − mean(pressure_i)over serving instances,with
pressure_ithe instance’s PressureScore. Connection-count imbalance is reported next to it, but drives nothing. -
Harm threshold: imbalance matters only when the busiest instance is saturated (pressure ≥
scale_out_above) and the mean is clearly lower (imbalance ≥ 0.25) for a scale-out window — then adding instances would not help, moving new work would. -
Mitigation, only if step 2 is seen under realistic load and hurts p99 or throughput, in this order: (a) pressure-biased accept — an instance whose loop lag is high yields new accepts for a moment (inherited queue: it simply does not call accept while saturated, so the idle peer takes the connection); (b) HTTP/2 / WebSocket specific placement. Established connections are never migrated. No TCP eBPF without evidence.
Consequences
- Imbalance is visible in
statusand metrics; whether it harms anything is decided by measurement (E3 benchmark: skewed long-lived H2/WebSocket mixes). - Nothing changes on the request path unless the measurement says so.