---
title: "WebSocket em Rust: guia com Axum e Tokio | Rust Brasil"
url: "https://rustlang.com.br/blog/websocket-rust-axum-tokio-producao-2026/"
markdown_url: "https://rustlang.com.br/blog/websocket-rust-axum-tokio-producao-2026.MD"
description: "Crie um servidor WebSocket em Rust com Axum e Tokio: conexões, broadcast, salas, heartbeat, autenticação e quando usar WebSocket, SSE ou polling."
date: "2026-09-30"
author: "Equipe Rust Brasil"
---

# WebSocket em Rust: guia com Axum e Tokio | Rust Brasil

Crie um servidor WebSocket em Rust com Axum e Tokio: conexões, broadcast, salas, heartbeat, autenticação e quando usar WebSocket, SSE ou polling.


**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](/tutoriais/api-rest-axum/) e do [Tokio](/ecossistema/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](/blog/tower-http-middleware-producao-axum-2026/)) | 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:

```toml
[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:

```rust
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.

```rust
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:

```rust
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:

```rust
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:

1. **Token curto na query string**, validado antes do `on_upgrade`, com expiração de minutos — o padrão mais comum;
2. **Cookie de sessão** enviado automaticamente pelo navegador no handshake;
3. **Token de uso único via REST**: o cliente pede um ticket em `/ws-ticket` autenticado por [JWT](/blog/autenticacao-jwt-rust-axum-2026/) 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 `SIGTERM` com `axum::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](/blog/prometheus-metrics-rust-axum-producao-2026/).

## 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](/blog/rust-redis-cache-backend-2026/), [NATS](/blog/rust-mensageria-kafka-rabbitmq-nats-2026/)). 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](/empresas/) avaliam em vagas sênior. Um chat com salas, autenticação, heartbeat e métricas publicados no seu [portfólio](/carreira/portfolio-github/) vale mais que dez CRUDs. Para as perguntas clássicas de entrevista, revise o [guia de perguntas e respostas](/blog/perguntas-respostas-entrevistas-rust-2026/).

## 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](/ecossistema/axum/), [tower-http em produção](/blog/tower-http-middleware-producao-axum-2026/), [background jobs com Tokio](/blog/rust-background-jobs-filas-tokio-2026/) e o [tutorial de API REST](/tutoriais/api-rest-axum/). Para aplicar tempo real no mercado, acompanhe as [vagas Rust](/vagas/) atualizadas diariamente.
