Concept — the serving model (process-per-unit-of-work)
A RUSM component can be a high-throughput HTTP / WebSocket / SSE server. The host owns the socket and the protocol; the guest just produces responses — in Rust or TypeScript. rusm-otp stays Wasm-free; all the serving machinery (hyper, tungstenite, wasi:http) lives only in rusm-wasm.
One shape, by design
Serving is always process-per-unit-of-work — there is no "resident" mode to choose:
- HTTP / SSE — a fresh, sandboxed WASM instance per request.
- WS — one sandboxed component process per connection.
This single choice buys properties you'd otherwise have to engineer around:
- No head-of-line blocking, ever. Each request gets its own instance, so a slow or blocking handler can't stall the next request.
- Crash containment. A trap fails just that request or socket — never the listener or the other clients. There is no shared instance to corrupt.
- Total isolation. Each instance has its own linear memory and its own default-deny capability profile.
- Cheap. Per-request instances ride the pooled spawn path (pooling allocator + CoW linear memory + precomputed export index), ~440k component spawns/sec; RSS tracks only live instances.
Where state goes
The serving instance is stateless and disposable. Anything that must outlive a request lives elsewhere — never in the serving instance:
- a
[components.<name>]service (resident = true) — a long-lived, supervised, stateful process reached over the actor API (whereis/call/send): a counter, cache, session map, pub/sub hub, chat-room registry; or - durable
kvfor state that must survive restarts.
This is where the old "resident vs per-call" decision now lives: a resident [components.<name>] service holds state; a worker spawns per call. Serving components are always per-request. A handler that needs state simply calls a service and shapes the reply.
Declarative routing
Routing lives in a per-listener rusm.toml [serve.routes] subtable — never in handler code. Each [[serve]] HTTP/SSE listener has its own [serve.routes], so multiple listeners (e.g. a public API and an admin port) route independently. A key is "METHOD /path/pattern", a value is "component#action":
:namecaptures a path parameter (read viaParams::get("name"));- a trailing
*captures the remaining segments; - the separator is
#(:and.are reserved by RUSM's scheme/file conventions).
Matching is by specificity (literal > :param > *). A path that matches but not for the method → 405; no match → 404.
Handlers are named actions
A Rust serving component is a module of pub fns under #[rusm_rs::handlers] — no main, no router, no wire plumbing. The macro generates the whole component shell and the action dispatch; the developer writes only handler functions:
- a 2-arg action
fn(Request, Params) -> Responseis buffered; - a 3-arg action
fn(Request, Params, Sse)streams SSE — and since each request is its own process, it may block for the whole connection.
TypeScript serving uses web standards instead (the macro is Rust): HTTP/SSE export default a fetch-shaped handler (SSE returns a ReadableStream body); WS uses export default websocket({ open, message }), one worker per connection.
How it works
- HTTP / SSE ride
wasi:http. The host gateway resolves the route, spawns the matched handler fresh, dispatches the action over the actor wire, and turns the reply into the response — buffered, or for SSE a chunked streamed body draining the guest's back-pressured byte stream (see byte streams). - WebSocket upgrades host-side; each inbound frame becomes a mailbox message, and replies go out through a Wasm-free writer process that owns the socket sink — one isolated process per connection.
- An ephemeral Wasm-free responder process owns the reply hand-off so the sandboxed guest never touches a socket.
- Standards-first: a stock
wasi:httpcomponent serves unchanged; therusm:runtimeactor world is opt-in.
Serving and RPC unify
A serving handler and an actor-world service are the same thing — a component exporting named functions. A handler action is reachable via an HTTP route; a service function via an actor call. Same wire, same spawn model. So "shared state" is just "a component you call."
How it's benchmarked (honestly)
Serving throughput is measured out-of-process by the rusm-loadtest binary against a real rusm serve port, so the load generator never steals the server's CPU and the number is the server's — see the benchmark reference.
See the full serving guide for routing syntax, the #[rusm_rs::handlers] macro, the Sse API, the TypeScript path, and a worked example; the [[serve]] and [serve.routes] schema is in the configuration reference.
Phase 11.
rusm servehostsrusm.toml [[serve]]entries on real ports; serving TLS is planned for Phase 12.