O que são WebSockets?
Um WebSocket é uma conexão persistente e full-duplex entre um cliente e um servidor. Após um handshake inicial via HTTP, ambos os lados podem enviar mensagens a qualquer momento através da mesma conexão TCP, com um overhead baixíssimo por mensagem. O servidor pode enviar dados no momento em que algo muda, em vez de esperar que o cliente faça a requisição.
Isso torna os WebSockets a ferramenta ideal para chats, indicadores de presença, dashboards em tempo real, edição colaborativa, jogos multiplayer e qualquer outra funcionalidade onde as atualizações devam chegar instantaneamente. Para atualizações simples de via única, Server-Sent Events costumam ser uma escolha mais simples.
O handshake de upgrade
Uma conexão WebSocket começa como uma requisição HTTP.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Se o servidor aceitar, ele responde:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Após o 101 Switching Protocols, a conexão deixa de ser HTTP. Ela se torna um WebSocket, e os dados fluem em frames. Como começa via HTTP, ela funciona nas mesmas portas (80 e 443), passa pela maioria dos proxies e firewalls, e faz o upgrade para TLS com wss://.
Frames
As mensagens WebSocket são divididas em frames com um pequeno cabeçalho. Existem frames de texto, frames binários e frames de controle para ping, pong e close. O framing é minimalista, e é por isso que as mensagens WebSocket são muito mais leves do que requisições HTTP repetidas.
O protocolo também define códigos de fechamento (close codes), como 1000 para um fechamento normal e 1001 para “going away”, que ajudam ambos os lados a entender por que uma conexão foi encerrada.
A API do navegador
A API do cliente é pequena e orientada a eventos.
// client.js
const socket = new WebSocket("wss://api.example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
render(message);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
});
socket.addEventListener("error", () => {
console.error("connection error");
});
Você envia com socket.send() e recebe eventos message. A propriedade readyState informa se o socket está conectando, aberto, fechando ou fechado.
Um servidor Node.js
A biblioteca ws é a maneira padrão de executar um servidor WebSocket no Node.
// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
socket.on("close", () => console.log("client left"));
});
O servidor rastreia cada conexão e pode enviar mensagens para um único cliente, um subconjunto (uma room) ou para todos eles. O Socket.IO é uma alternativa de nível mais alto que adiciona rooms, acknowledgements, reconexão automática e fallbacks, ao custo de um protocolo e cliente customizados.
Salas e presença
Aplicações reais raramente transmitem mensagens para todos. Você rastreia qual conexão pertence a qual usuário ou sala.
// rooms.js
const rooms = new Map();
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
}
function broadcast(room, payload) {
for (const socket of rooms.get(room) ?? []) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
Salas, presença (quem está online) e indicadores de digitação são todos construídos a partir deste padrão, somados a heartbeats para detectar desconexões.
Heartbeats e reconexão
Uma conexão pode cair sem que nenhum dos lados perceba, especialmente em redes móveis ou atrás de proxies. Duas coisas a mantêm saudável:
- Heartbeats. Envie um ping a cada 30 segundos e encerre a conexão se nenhum pong for recebido. Muitos proxies também fecham conexões inativas, portanto, o tráfego as mantém abertas.
- Reconexão. O cliente deve tentar reconectar usando exponential backoff e restabelecer suas inscrições. É aqui que uma biblioteca como Socket.IO poupa esforço.
// heartbeat.js
const interval = setInterval(() => {
for (const socket of wss.clients) {
if (socket.isAlive === false) return socket.terminate();
socket.isAlive = false;
socket.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
Escalabilidade
Uma conexão WebSocket reside em exatamente uma instância de servidor, portanto, realizar broadcasts entre instâncias exige um canal compartilhado.
- Pub/sub: publique mensagens no Redis, NATS ou outro broker, e faça com que cada instância as entregue aos seus clientes locais.
- Sticky sessions: se a sua infraestrutura exigir, mantenha o cliente na mesma instância durante toda a vida útil da conexão.
- Estado em um store compartilhado: mantenha a presença e a participação em salas no Redis para que qualquer instância possa responder.
- Backpressure e limites: limite as conexões por instância e monitore a memória, pois cada conexão mantém um socket e algum estado.
Segurança
- Sempre utilize
wss://em produção para que o tráfego seja criptografado. - Realize a autenticação durante o handshake e rejeite conexões não autorizadas.
- Valide o header
Originpara evitar o cross-site WebSocket hijacking. - Autorize cada mensagem; nunca presuma que um cliente conectado tem permissão para realizar determinada ação.
- Limite o tamanho e a frequência das mensagens para evitar abusos.
- Configure timeouts para que conexões abandonadas não causem vazamentos de memória (leaks).
Melhores práticas
- Use WebSockets para atualizações bidirecionais de baixa latência e SSE para push unidirecional.
- Implemente heartbeat e reconexão; nunca assuma que uma conexão está ativa.
- Autentique durante o handshake e autorize cada mensagem.
- Mantenha o estado por conexão reduzido e utilize rooms em um store compartilhado.
- Use pub/sub para realizar broadcasts entre instâncias.
- Defina limites de tamanho de mensagem e rate limits.
- Encerre conexões inativas e monitore a contagem de conexões.
Erros comuns
- Usar WebSockets para requisições e respostas simples onde o HTTP seria suficiente.
- Esquecer os heartbeats e deixar conexões mortas abertas (leaking).
- Fazer broadcast para todos os clientes quando apenas uma room deveria receber.
- Armazenar o estado autoritativo apenas em memória e perdê-lo ao reiniciar.
- Ignorar verificações de origin e a autorização de mensagens.
- Assumir que o load balancer preservará a conexão sem a configuração adequada.
Próximos passos
WebSockets são a camada de conexão persistente sobre o TCP. Consolide seus conhecimentos com o guia de HTTP e TCP/IP & Sockets, execute-os no Node.js e compare o modelo de push com GraphQL subscriptions. Depois, construa uma pequena sala de chat com salas e heartbeats para ver esses padrões na prática.