Overview
Server push in Rust rides directly on Tokio's cheap tasks, the same economics that
make goroutine-per-connection work in Go: one task per
connection, channels to fan events in and out. tungstenite is the WebSocket
protocol implementation everyone shares — tokio-tungstenite is its async form,
used standalone for clients and raw servers — while the web frameworks ship their own
ergonomic upgrades on top: axum's WebSocketUpgrade extractor and Sse response,
actix's actix-ws. SSE remains the right default when the flow is one-directional.
Key points
- tokio-tungstenite is the base layer: a
WebSocketStreamimplementingStream + Sinkover messages —client_asyncfor outbound connections (or via reqwest'swebsocket()upgrade),accept_asyncfor bare servers; the frameworks wrap this or equivalents. - axum's built-in: a handler takes
WebSocketUpgradeand returnsws.on_upgrade(|socket| async { … }); the socket splits (socket.split()) into sender/receiver halves so reading and writing run as separate tasks — the canonical shape. - The state pattern is channels, not a registry object: each connection task owns
its socket; a shared
broadcastchannel (chat/fanout) or per-connectionmpscsenders in aDashMap(targeted push) route messages — ownership makes the "who may write to this socket" question explicit where Go's gorilla answers it with a one-writer convention. - Backpressure is built into the model:
Sink::sendawaits; a slow client slows its own task, andbroadcast's bounded buffer surfaces lag asLaggederrors you handle (skip or disconnect) — the decision Go leaves to bufio and your discipline. - SSE in axum is a response type:
Sse::new(stream)overResult<Event, _>items with automatic keep-alives — pair with abroadcastsubscriber stream; one-directional, proxy-friendly, auto-reconnecting viaLast-Event-ID. - actix-ws replaces the old actor-based
actix-web-actors— plain async handlers now; the actor framework remains available where a session registry genuinely wants supervised state. - fastwebsockets (Deno's) is the performance outlier for raw throughput; graphql-ws rides this layer for GraphQL subscriptions.
- Hardening: authenticate at upgrade time (the HTTP request still has headers),
bound message sizes (
max_message_size), idle-timeout withtokio::select!+ interval pings — see Web security.
Examples
async fn ws_handler(ws: WebSocketUpgrade, State(st): State<AppState>) -> Response {
ws.on_upgrade(move |socket| handle(socket, st))
}
async fn handle(socket: WebSocket, st: AppState) {
let (mut tx, mut rx) = socket.split();
let mut events = st.events.subscribe(); // tokio::sync::broadcast
loop {
tokio::select! {
Ok(ev) = events.recv() => {
if tx.send(Message::text(ev)).await.is_err() { break }
}
msg = rx.next() => match msg {
Some(Ok(Message::Text(t))) => st.handle_inbound(t).await,
_ => break, // closed or errored
},
}
}
}
Related
- Backend protocols in Rust — the map — parent map.
- Axum deep dive — the upgrade extractor's home.
- Async runtimes — the task/channel economics underneath.
- GraphQL in Rust — subscriptions over this transport.
- WebSockets & SSE in Java and in Go — the counterparts; Rust's ownership turns their concurrency conventions into compiler-checked structure.