Para tempo real em Rust em 2026, a combinação padrão é Axum com a feature ws sobre o Tokio: o upgrade do protocolo cabe em um handler, cada conexão vira uma task leve e o tokio::sync::broadcast resolve o envio para vários clientes. Use WebSocket quando o cliente também envia mensagens (chat, colaboração, jogos); use SSE quando o fluxo é quase todo servidor→cliente; use polling quando o dado muda raramente. Este guia mostra o servidor mínimo, broadcast entre conexões, salas, heartbeat e o checklist de produção — montado sobre a mesma stack do guia de API REST com Axum e do Tokio.
Resposta rápida: WebSocket, SSE ou polling?
| Situação | Escolha | Por quê |
|---|---|---|
| Chat, notificação bidirecional, jogo multiplayer | WebSocket | canal full-duplex persistente, latência mínima |
| Feed, ticker, progresso de job, dashboard | SSE | HTTP simples, reconexão automática, cache e proxy amigáveis |
| Dado que muda a cada minutos/horas | polling | sem estado no servidor, zero complexidade extra |
| Cliente envia pouco, servidor empurra muito | SSE (ou WebSocket com tower-http) | SSE evita infraestrutura de upgrade |
| Atravessar ambientes hostis a upgrade | SSE | proxies corporativos ainda bloqueam WebSocket às vezes |
| Protobuf/binary em alta frequência | WebSocket com Message::Binary | sem overhead de texto por frame |
A regra prática: não abra WebSocket para economizar requisições que não existem. Cada conexão persistente ocupa task, memória e atenção operacional. Abra quando a bidirecionalidade e a latência realmente pagarem a conta.
Servidor WebSocket mínimo com Axum
O Cargo.toml com as dependências essenciais:
[dependencies]
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
futures-util = "0.3"
O servidor echo completo cabe em poucas linhas — o Axum cuida do handshake HTTP e entrega um WebSocket pronto:
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::Response,
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new().route("/ws", get(ws_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn ws_handler(ws: WebSocketUpgrade) -> Response {
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 {
if socket.send(Message::Text(text)).await.is_err() {
break;
}
}
}
}
socket.recv() devolve Option<Result<Message, _>>: None encerrou a conexão, Err indicou falha de protocolo. Em ambos os casos, sair do loop encerra a task e libera os recursos — o Tokio faz o resto.
Broadcast: enviando para todos os clientes
Um servidor real mantém dezenas ou milhares de conexões que precisam receber as mesmas mensagens. O tokio::sync::broadcast é a ferramenta natural: cada conexão faz subscribe() e roda uma task dedicada de envio.
use axum::{extract::ws::{Message, WebSocket, WebSocketUpgrade}, response::Response, extract::State};
use futures_util::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::sync::broadcast;
#[derive(Clone)]
struct AppState {
tx: broadcast::Sender<String>,
}
async fn ws_handler(State(state): State<AppState>, ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();
let mut rx = state.tx.subscribe();
let mut send_task = tokio::spawn(async move {
while let Ok(msg) = rx.recv().await {
if sender.send(Message::Text(msg.into())).await.is_err() {
break; // cliente desconectou
}
}
});
let tx = state.tx.clone();
let mut recv_task = tokio::spawn(async move {
while let Some(Ok(Message::Text(text))) = receiver.next().await {
// Ignora erro: sem assinantes, não há a quem enviar
let _ = tx.send(text.to_string());
}
});
// Se qualquer task termina, a conexão acabou: cancele a irmã
tokio::select! {
_ = &mut send_task => recv_task.abort(),
_ = &mut recv_task => send_task.abort(),
}
}
Dois detalhes que evitam sustos: o canal broadcast tem capacidade fixa — um cliente lento que não consumir o buffer recebe RecvError::Lagged e deve decidir entre sincronizar estado via REST ou reconectar. E select! garantindo o abort() da task irmã impede vazamento de tasks para conexões mortas.
Salas e canais separados
Para chat com salas, troque o Sender global por um mapa concorrente com um canal por sala:
use dashmap::DashMap;
use std::sync::Arc;
#[derive(Clone)]
struct Rooms {
map: Arc<DashMap<String, broadcast::Sender<String>>>,
}
impl Rooms {
fn join(&self, room: &str) -> broadcast::Receiver<String> {
self.map
.entry(room.to_string())
.or_insert_with(|| {
let (tx, _rx) = broadcast::channel(64);
tx
})
.subscribe()
}
}
Crie a rota como /ws/{room} (sintaxe de parâmetro do Axum 0.8), extraia o nome com Path<String> e chame rooms.join(&room) no upgrade. Quando o último assinante de uma sala sai, remova o Sender do mapa para não acumular canais órfãos.
Heartbeat: pings que mantêm a conexão viva
Proxies, load balancers e plataformas de PaaS fecham conexões ociosas — os timeouts comuns variam de 30 a 120 segundos. O servidor precisa pingar antes disso:
use std::time::Duration;
async fn pump(socket: WebSocket) {
let (mut sender, mut receiver) = socket.split();
let mut tick = tokio::time::interval(Duration::from_secs(25));
loop {
tokio::select! {
_ = tick.tick() => {
if sender.send(Message::Ping(vec![].into())).await.is_err() {
break;
}
}
msg = receiver.next() => match msg {
Some(Ok(_)) => {} // qualquer quadro renova a conexão
_ => break,
},
}
}
}
Navegadores respondem pings automaticamente no nível do protocolo — não é preciso tratar Message::Pong à mão. Clientes nativos, porém, podem exigir que sua aplicação responda. Também vale um tokio::time::timeout envolvendo o recebimento: cliente vivo responde, cliente morto estoura o timeout e libera a task.
Autenticação e o checklist de produção
O navegador não permite definir cabeçalhos de autenticação no handshake do WebSocket. As saídas práticas:
- Token curto na query string, validado antes do
on_upgrade, com expiração de minutos — o padrão mais comum; - Cookie de sessão enviado automaticamente pelo navegador no handshake;
- Token de uso único via REST: o cliente pede um ticket em
/ws-ticketautenticado por JWT e troca pela conexão.
Com autenticação resolvida, o checklist que separa uma demo de um serviço pronto:
- Valide antes do upgrade — negar após a conexão abre custe a reconexão em loop;
- Limite conexões por usuário/IP — sem limite, um cliente abre milhares de sockets;
- Limite o tamanho de mensagens — payloads gigantes em texto derrubam memória;
- Broadcast com buffer dimensionado — e política explícita para
Lagged; - Heartbeat + timeout de leitura — para colher conexões mortas;
- Shutdown gracioso — trate
SIGTERMcomaxum::serve(...).with_graceful_shutdown, fechando canais e deixando clientes reconectarem; - Métricas por estado da conexão — conexões ativas, mensagens por segundo e desconexões por motivo, no formato do guia de Prometheus com Axum.
Erros comuns
Bloquear a task do socket
Chamar código bloqueante (std::thread::sleep, I/O síncrono, bcrypt) dentro do handler congela toda a conexão. Use tokio::task::spawn_blocking para trabalho pesado e mantenha o loop do socket enxuto.
Estado global mutável sem concorrência pensada
Um Arc<Mutex<HashMap<…>>> disputado por todas as conexões vira gargalo. Prefira broadcast, watch ou DashMap por sala — cada padrão existe para um formato de fan-out.
Ignorar Lagged do broadcast
Cliente lento perdeu mensagens e o canal não reenvia. Trate o erro sincronizando o estado atual via REST em vez de fingir que nada aconteceu.
Um canal broadcast gigante para tudo
Canal com capacidade alta esconde o problema de backpressure e aumenta a memória por assinante. Dimensione pelo ritmo real de mensagens da sala.
Reconexão sem backoff no cliente
Servidor reiniciado + clientes reconectando em loop = tempestade de handshakes. Exponential backoff com jitter é obrigatório no cliente de produção.
Esquecer que WebSocket é estado
Uma API REST escala adicionando réplicas; um cluster WebSocket precisa de stickiness no balanceador ou de um barramento entre instâncias (Redis pub/sub, NATS). Decida isso antes do segundo replica.
WebSocket como sinal de carreira
Tempo real é um dos projetos que mais aparecem em entrevistas de backend Rust no Brasil: cobre tarefas assíncronas, canais, concorrência, timeout e operação sob carga — exatamente o que times de fintechs e empresas que usam Rust avaliam em vagas sênior. Um chat com salas, autenticação, heartbeat e métricas publicados no seu portfólio vale mais que dez CRUDs. Para as perguntas clássicas de entrevista, revise o guia de perguntas e respostas.
Perguntas frequentes
Como criar um WebSocket em Rust?
Use o Axum com a feature ws sobre o runtime Tokio. Registre uma rota GET com WebSocketUpgrade, chame on_upgrade para receber o socket e processe Message::Text e Message::Binary em um loop assíncrono. O Axum delega o protocolo ao crate ws, enquanto o Tokio cuida das tasks de envio e recebimento.
WebSocket ou SSE: qual escolher?
Escolha WebSocket quando o cliente também precisa enviar mensagens, como chat, jogos e edição colaborativa. Escolha Server-Sent Events quando o fluxo é predominantemente do servidor para o cliente, como feeds, notificações e dashboards, porque SSE é HTTP simples, reconecta sozinho e atravessa proxies com menos configuração. Se o cliente só busca dados de tempos em tempos, polling simples basta.
Como autenticar uma conexão WebSocket no navegador?
O navegador não permite definir cabeçalhos Authorization no handshake do WebSocket. As opções práticas são validar um token curto na query string durante o upgrade, depender do cookie de sessão existente, ou emitir um token de uso único via REST e trocá-lo pela conexão WebSocket. Nunca aceite token de longa duração na URL sem expiração curta e rotação.
Como enviar mensagens para vários clientes em Rust?
Use um tokio::sync::broadcast. Cada conexão faz subscribe e mantém uma task de envio consumindo o canal, enquanto uma task de recebimento publica as mensagens recebidas. Para salas ou canais separados, guarde um Sender por sala em um mapa concorrente como DashMap e faça subscribe no momento do upgrade.
Por que minha conexão WebSocket cai atrás de load balancer?
Porque proxies e balanceadores fecham conexões ociosas, normalmente entre 30 e 120 segundos. Envie pings periódicos do servidor com Message::Ping em um tokio::time::interval, trate a resposta como sinal de vida, defina timeout de leitura e verifique o limite de idle timeout do seu proxy ou provedor.
Como testar WebSocket em Rust?
Em testes de integração, suba a aplicação com Tokio e conecte um cliente como tokio-tungstenite, validando handshake, troca de mensagens, desconexão e broadcast entre dois clientes. Em unit tests, teste handlers e regras de sala com traits e fakes, sem depender de rede real.
Conclusão
WebSocket em Rust deixa de ser assustador quando você enxerga o modelo: um handler faz o upgrade, cada conexão vira duas tasks pequenas ligadas por canais, e a produção se resolve com heartbeat, limites e shutdown gracioso. Axum e Tokio cobrem todo o caminho — do echo de 20 linhas ao chat com salas atrás de load balancer. Continue com o guia do Axum, tower-http em produção, background jobs com Tokio e o tutorial de API REST. Para aplicar tempo real no mercado, acompanhe as vagas Rust atualizadas diariamente.