rgoussu@goussu: ~/library/rust/protocols
~/library/rust/protocols cat websockets.md

WebSockets & SSE in Rust

# tokio-tungstenite as the protocol workhorse, axum's WebSocketUpgrade and Sse built-ins, actix-ws, connection-state patterns with channels, and backpressure on Tokio.

Conceptsaved 2026-08-09 #rust#protocols#websockets#sse#tokio

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 WebSocketStream implementing Stream + Sink over messages — client_async for outbound connections (or via reqwest's websocket() upgrade), accept_async for bare servers; the frameworks wrap this or equivalents.
  • axum's built-in: a handler takes WebSocketUpgrade and returns ws.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 broadcast channel (chat/fanout) or per-connection mpsc senders in a DashMap (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::send awaits; a slow client slows its own task, and broadcast's bounded buffer surfaces lag as Lagged errors you handle (skip or disconnect) — the decision Go leaves to bufio and your discipline.
  • SSE in axum is a response type: Sse::new(stream) over Result<Event, _> items with automatic keep-alives — pair with a broadcast subscriber stream; one-directional, proxy-friendly, auto-reconnecting via Last-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 with tokio::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