Phase 5 — Connectivity: TCP
A connection is just a process — Phase 5 maps TCP sockets onto the actor model, giving every connection its own isolation, supervision, and crash recovery for free.
Why this phase
The actor model's payoff for networking is not a clever abstraction — it's structural. Accept a socket, spawn a process to own it. That process has a mailbox, can be killed, can be supervised, and can crash without touching any other connection. The OS hands you a socket; RUSM hands you a process. Everything from Phase 3 applies immediately.
Without this, a process system is a closed universe. With it, RUSM processes speak to the outside world — and the outside world is handled with the same fault-tolerance model as everything else.
What shipped
listen(addr, handler) -> (SocketAddr, ProcessHandle)— binds aTcpListenerand runs an acceptor process. Every accepted socket is spawned as its own process runninghandler(ctx, stream). Returns the bound address (handy with port 0) and a handle to the acceptor — kill it to stop listening, killing the port.connect(addr) -> io::Result<TcpStream>— opens an outbound TCP connection.- Connection-storm engine (
rusm-bench) — a ramp-and-hold load reporting real connections/sec and peak concurrency.
Design highlights
- The OS is the limit, not RUSM. RUSM mints processes far faster than any TCP stack hands out sockets. Sustained connection rate is bounded by the kernel's handshake/ephemeral-port/
TIME_WAITbudget — the same ceiling every runtime hits. RUSM measures it honestly (~6–16k/s on loopback) rather than inflating the number. SO_LINGER(0)to avoidTIME_WAITexhaustion. An early version showed a 291/s sawtooth: client active-close piled upTIME_WAITsockets. Closing with a reset viasocket2::SockReffrees them immediately, giving a sustained rate instead of a collapsing one — a real production correctness fix.- Ramp-and-hold, not flood. Flooding with parallel connectors exploded latency (the OS serializes handshakes anyway); a steady ramp measures the true ceiling. The fd limit is raised at startup via the
rlimitcrate. - Kill the acceptor, kill the port. The acceptor process owns the
TcpListener. Drop the handle → the process exits → the listener drops → the port closes. No separate lifecycle to manage.
What this unlocks
Any network protocol can be served by spawning a handler process per connection: HTTP, WebSocket, custom binary protocols. Each connection is isolated, supervised, and killable. A misbehaving client crashes its process; other connections are unaffected.
This is the direct predecessor to Phase 9's cluster transport (QUIC instead of TCP, for per-stream HoL-block freedom) and Phase 11's HTTP/WS/SSE serving model — process-per-request and process-per-connection all the way down.
This phase also completes the Wasm-free OTP core: rusm-otp has zero wasmtime dependency and stands as a fully capable actor runtime on its own.
Try it
cargo run -p rusm-bench -- run connection-storm 5 # real sustained connections; watch the rate plateauStatus
Phase complete. Connection-storm is live in the dashboard. The Wasm-free invariant holds: rusm-otp carries no wasmtime dependency.
Next: Phase 6 — Wasmtime as the process backend: swap a native Rust body for a sandboxed Wasm instance, behind the same spawn() call.