WebSockets & SSE
Push live updates to clients with WebSockets for bidirectional chat and Server-Sent Events for one-way streams.
Recipe
Quick-reference recipe card - copy-paste ready.
use axum::{extract::ws::{Message, WebSocket, WebSocketUpgrade}, response::IntoResponse, routing::get};
async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(Ok(msg)) = socket.recv().await {
if let Message::Text(text) = msg {
let _ = socket.send(Message::Text(format!("echo: {text}"))).await;
}
}
}When to reach for this: Clients need live data without polling - chat, dashboards, or progress streams.
Working Example
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::sse::{Event, KeepAlive, Sse},
routing::get,
Router,
};
use futures_util::stream::{self, Stream};
use std::{convert::Infallible, time::Duration};
use tokio_stream::StreamExt as _;
async fn ws_upgrade(ws: WebSocketUpgrade) -> impl axum::response::IntoResponse {
ws.on_upgrade(handle_ws)
}
async fn handle_ws(mut socket: WebSocket) {
while let Some(Ok(msg)) = socket.recv().await {
if let Message::Text(t) = msg {
if socket.send(Message::Text(format!("echo: {t}"))).await.is_err() {
break;
}
}
}
}
async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let stream = stream::iter(0..5).map(|i| {
Ok(Event::default().data(format!("tick {i}")))
});
Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(15)))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/ws", get(ws_upgrade))
.route("/events", get(sse_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}What this demonstrates:
WebSocketUpgradeperforms the HTTP upgrade handshake.- Echo loop over
Message::Textframes. SsestreamsEventitems with keep-alive pings.- Both endpoints coexist in one Axum router.
Deep Dive
How It Works
- WebSockets start as HTTP GET with
Upgrade: websocket; Axum handles the handshake. - SSE uses
text/event-streamand unidirectional server-to-client messages. - Long-lived connections need heartbeat/ping to survive proxies and load balancers.
When to Pick Each
| Transport | Direction | Fit |
|---|---|---|
| WebSocket | Bidirectional | Chat, games, collaborative editing |
| SSE | Server to client | Live feeds, notifications, metrics |
| Long poll | Request/response | Legacy clients without SSE |
Rust Notes
// Broadcast channel fan-out to many WebSocket clients
let (tx, _) = tokio::sync::broadcast::channel::<String>(100);Gotchas
- No heartbeat - Proxies close idle WebSockets after 60s. Fix: Send periodic ping frames or SSE comments.
- Blocking the socket loop - Heavy work in
handle_wsstalls all messages. Fix:tokio::spawnper connection task with channels. - Unbounded memory - Slow clients fill internal buffers. Fix: Use bounded channels; drop or disconnect lagging clients.
- Auth only at upgrade - Token checked once then connection lives forever. Fix: Revalidate periodically or use short-lived tokens.
- SSE through buffering proxies - Nginx may buffer events. Fix: Disable buffering with
X-Accel-Buffering: no.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Polling | Simple, rare updates | Sub-second latency requirements |
| gRPC streaming | Service-to-service | Browser clients without gRPC-Web |
tokio-tungstenite standalone | Non-Axum servers | Already on Axum |
FAQs
WebSocket binary messages?
Handle Message::Binary for protobuf or compressed payloads.
SSE reconnect?
Clients auto-reconnect; include id field for resume semantics.
Scale WebSockets?
Sticky sessions or shared pub/sub (Redis) across instances.
CORS for WebSockets?
Browsers check Origin during handshake - validate in upgrade handler.
Backpressure?
Check send errors and close socket when client disconnects.
TLS?
Terminate at load balancer; Axum sees plain HTTP behind proxy.
Test WebSockets?
Use tokio-tungstenite client against test server.
SSE content type?
Axum Sse sets headers automatically.
Combine with REST?
Same router - REST for CRUD, /ws or /events for live updates.
axum-extra WebSocket?
Axum 0.8 includes extract::ws - no separate crate required.
Related
- WebSockets -
tokio-tungsteniteclients - Middleware & Tower - Timeouts for long connections
- Testing Web Services - Integration tests
- HTTP Clients (reqwest) - Non-browser clients
- Authentication & Sessions - Securing upgrades
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.