net::serve Below Every Protocol, Admission by an Exchangeable Strategy, and a Panic That Ends Only Its TaskStatus: Accepted
Date: October 6, 2026 (#121; D6-D9 the same day)
Answers: #121,
the server takes many connections at once
Related:
ADR-289 D6-D15 (std::net, the http package, its caps);
ADR-303 (the runtime, readiness for sockets, the executor at both settings);
ADR-297 (cleanup on the way out);
ADR-327 (shared state after a panic, memory budgets);
Part II 11-12, Part III A.2
How does a server take many connections at once, so that a small service - one route, a page of API documentation, a protocol of its own - does not run into a dead end, a busy machine is not overrun, and one bad request does not take the server down?
The http package takes one connection at a time. std::net already gives
the thread up while a socket waits, and the executor runs many tasks at both
values of user_parallelism (ADR-303); nothing starts one task
per connection. A panic at user_parallelism = no ends the process.
| Tool | Accepting | Limit on connections | A crash in one connection |
|---|---|---|---|
| Erlang/Elixir (Ranch, Cowboy) | an acceptor pool below every protocol; a protocol is a module | max_connections, configured, 1024 by default |
ends that process |
Go net/http |
a goroutine per connection | none; the operating system says no | ends the process unless recover is written |
| Rust (tokio, hyper, tonic) | the program’s loop, spawn per connection |
none built in | ends that task |
| Netflix concurrency-limits, Envoy | - | computed from measured latency; the algorithm is an exchangeable Limit (Vegas, Gradient, AIMD, Fixed) |
- |
From memory, not checked against a version. What transfers: Ranch’s layering, the adaptive limit and its exchangeable algorithm, and a crash that ends only its own unit of work.
| Option | Why it lost |
|---|---|
Many connections inside http::listen |
every other protocol - WebSocket, gRPC, one of the program’s own - writes the loop, the limits and the timeouts again |
| A maximum number of connections as an option or in a configuration file | how many connections a machine serves depends on the load at the moment; a number fixed in advance is wrong at most moments |
| An adaptive limit computed on every request | measuring in the hot path costs most when the machine has least to spare |
A panic ends the process at no |
one malformed request ends every connection |
std::net::serve accepts connections and runs each in a task of its own; every protocol stands on it.net::serve("0.0.0.0:7000") fn(conn) { echo(conn) }
http::listen("0.0.0.0:8080") fn(request) { … } // net::serve, and HTTP/1.1 per connection
It is the one place that accepts, limits and stops; http is one of its users,
and WebSocket, gRPC or a program’s own protocol are others.
Changes ADR-289 D13: there is no connection cap.
serve takes an admission: strategy, a value implementing net::Admission:
pub trait Admission {
fn decide(ref mut self, load: net::Load) -> net::Decision sync
}
decide runs on the runtime’s side, on a beat it chooses itself: Load carries
what the runtime counts anyway - connections open, accepted and finished, the
tasks waiting to run, the descriptors in use and allowed - and Decision the
limit until the next decision and when that is. Per connection the hot path
compares and counts, nothing else. std provides the default, which adapts the
limit to the load; a package may provide another, a fixed number read from a
file among them. Load grows and never shrinks.
Not more descriptors than the system allows; at the ceiling or over the limit
the server stops accepting and the system’s queue holds the next connection - it
never accepts to fail, and a ceiling never ends the server. A strategy is sync.
user_parallelism.Changes Part III A.2’s no row.
The task’s cleanups run (ADR-297), the panic hook runs first as on
every panic, and the program goes on. A panic in main ends the program. Where
the machine cannot unwind - wasm32-unknown traps - the module ends, as before.
For serve, the connection is closed; http answers 500 where the response
has not begun.
What a panic leaves in shared state is ADR-327’s.
connections: on http::listen stays, as what it is: a number of connections after which the server returns.A test’s tool, not a limit.
head_timeout, ten seconds by default.Changes ADR-289 D13: head_wait, a count of reads, is gone.
http::listen(at; head_timeout: time::Duration = 10.seconds()). The clock runs
from the accept for a connection’s first request and from the first byte for
every later one, and covers the whole head: a client that sends a byte now and
then is held to it as one that sends nothing. Past it the server answers 408
and closes the connection. A fixed default fits here, where the number of
connections did not: it says how long a client may take, which is the
protocol’s, not the machine’s load.
body_bytes_per_second, 500 by default, on average since it began.http::listen(at; body_bytes_per_second: i64 = 500). The rate is the bytes so
far over the time since the body’s first byte, checked after a start-up period
of std’s, so a burst or a stall of the network is not a refusal, a slow upload
that keeps coming is never cut off, and a body sent a byte at a time is. Past
it the server answers 408 and closes. The unit is in the name because no type
carries it.
net::serve gives every protocol a deadline per read and sets no idle limit of
its own: whether a silent connection is waiting or dead only the protocol
knows. A connection handed to another protocol - a WebSocket after its upgrade -
is no longer HTTP’s, and its owner decides how it knows the peer is alive,
typically by a heartbeat.
idle_timeout 75 seconds between requests, and closed early only after a response.Changes ADR-289 D15: not Connection: close after every response.
http::listen(at; idle_timeout: time::Duration = 75.seconds()). A connection
waits for its next request this long, and then closes. The default lies above
the 60 seconds of common load balancers, because a service that closes a quiet
connection before the balancer in front of it does loses the request the
balancer sends into it at that moment. Under load the admission strategy (D2)
may end connections early, and only in order: the next response on such a
connection says Connection: close; a quiet connection is never closed into
its silence before idle_timeout.
Load and Decision are a public interface; a strategy at yes
runs on another thread and must cross threads..route, OpenAPI), which needs reflection over
signatures that ADR-304 does not give.