O WebSocket Nativo no Node.js permite criar clientes de comunicação bidirecional usando a classe global WebSocket, sem instalar um pacote para o caso básico. A interface é compatível com a API dos navegadores e usa eventos como open, message, error e close.
A classe foi adicionada no Node.js 20.10.0 e 21.0.0, deixou de exigir a flag experimental no Node.js 22.0.0 e tornou-se estável no Node.js 22.4.0. No Node.js 26.5.0, continua disponível globalmente, embora possa ser desativada pela opção --no-experimental-websocket.
Neste guia, você aprenderá a conectar, enviar texto e bytes, receber mensagens, usar subprotocolos, autenticar, implementar heartbeat, reconectar com backoff, limitar filas, fechar corretamente, testar e entender por que a API nativa é um cliente, não um servidor WebSocket.
O que é o WebSocket nativo?
É uma implementação compatível com a Web da classe WebSocket. A documentação oficial do WebSocket global no Node.js apresenta o histórico e a disponibilidade. A documentação da API WebSocket na MDN explica propriedades, eventos e métodos.
Para conceitos de protocolo e servidores, consulte WebSocket com Node.js. Para o modelo de eventos usado pela classe, veja EventTarget no Node.js. O artigo de HTTPS e TLS no Node.js ajuda a proteger conexões wss:.
Cliente, não servidor
A classe global abre uma conexão para um servidor existente:
const socket = new WebSocket(
'wss://example.com/realtime'
);Ela não cria um servidor que aceita upgrades HTTP. Para servir WebSocket, use uma biblioteca ou infraestrutura com suporte ao lado servidor.
Evento open
socket.addEventListener('open', () => {
console.log('Conectado');
});Somente depois de open a conexão está pronta para envio normal.
Enviando texto
socket.addEventListener('open', () => {
socket.send(JSON.stringify({
type: 'subscribe',
channel: 'orders'
}));
});Defina um protocolo de aplicação com campo de versão, tipo e payload validado.
Recebendo mensagens
socket.addEventListener('message', event => {
console.log('Recebido:', event.data);
});event.data pode ser string ou dados binários, conforme a mensagem e a configuração.
Validando JSON
socket.addEventListener('message', event => {
if (typeof event.data !== 'string') return;
let message;
try {
message = JSON.parse(event.data);
} catch {
return;
}
if (!isValidMessage(message)) return;
handleMessage(message);
});Nunca confie em mensagens recebidas, mesmo quando o servidor pertence à mesma empresa.
Mensagens binárias
socket.binaryType = 'arraybuffer';
socket.addEventListener('message', event => {
if (event.data instanceof ArrayBuffer) {
const bytes = new Uint8Array(event.data);
processBytes(bytes);
}
});Defina limites antes de analisar imagens, arquivos ou protocolos binários.
Enviando bytes
const bytes = new Uint8Array([1, 2, 3]);
socket.send(bytes);Buffer e TypedArrays precisam ser testados conforme o contrato esperado pelo servidor. Consulte Buffer no Node.js.
Estados da conexão
A propriedade readyState pode ter estados equivalentes a:
CONNECTING;OPEN;CLOSING;CLOSED.
if (socket.readyState === WebSocket.OPEN) {
socket.send(message);
}Evento close
socket.addEventListener('close', event => {
console.log({
code: event.code,
reason: event.reason,
clean: event.wasClean
});
});O código ajuda a decidir se a reconexão é apropriada. Não repita indefinidamente quando o servidor rejeita autenticação ou protocolo.
Fechando corretamente
socket.close(1000, 'Encerramento normal');O código 1000 representa fechamento normal. A razão possui limite de tamanho e deve ser texto válido.
Evento error
socket.addEventListener('error', event => {
logger.error('websocket_error');
});O evento pode não expor todos os detalhes da falha. Use logs do servidor, métricas e o evento close para diagnóstico.
Subprotocolos
const socket = new WebSocket(
'wss://example.com/realtime',
['json.v2', 'json.v1']
);O servidor seleciona um protocolo suportado. Após open:
console.log(socket.protocol);Valide que o protocolo escolhido é conhecido antes de processar mensagens.
Autenticação
A API de navegador não permite configurar headers arbitrários no construtor da mesma forma que um cliente HTTP tradicional. Estratégias comuns incluem:
- cookie já estabelecido;
- token de curta duração na URL;
- subprotocolo controlado;
- mensagem de autenticação logo após open.
Evite tokens permanentes em query strings, porque URLs aparecem em logs.
Token temporário
Uma API HTTPS pode emitir um ticket de uso único e curta validade. O cliente usa esse ticket para abrir o WebSocket. Consulte Fetch Nativo no Node.js.
wss em produção
Use wss: para proteger tráfego e credenciais. Valide certificados e hostname. Não desative segurança TLS para contornar ambientes mal configurados.
Origem
Servidores WebSocket podem validar o header Origin. Um cliente Node.js não está sujeito ao mesmo modelo de segurança do navegador, portanto o servidor não deve depender apenas de Origin para autenticação.
Heartbeat
Proxies e balanceadores podem encerrar conexões ociosas. A API WebSocket de alto nível não expõe necessariamente frames ping e pong como bibliotecas específicas. Implemente heartbeat no protocolo de aplicação quando necessário:
socket.send(JSON.stringify({
type: 'ping',
time: Date.now()
}));Resposta pong
if (message.type === 'pong') {
lastPongAt = Date.now();
}Feche e reconecte quando o prazo for excedido. Não deixe timers sem cleanup.
Timer de heartbeat
const timer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
sendHeartbeat();
}
}, 30000);
timer.unref();Limpe no close. Veja Timers Promises no Node.js para loops canceláveis.
Reconexão
WebSocket não reconecta automaticamente. Implemente uma máquina de estado.
async function reconnect(attempt, signal) {
const maximum = 30000;
const base = Math.min(
500 * 2 ** attempt,
maximum
);
const wait = Math.random() * base;
await delay(wait, undefined, { signal });
return connect();
}Backoff com jitter
Jitter evita que milhares de clientes reconectem no mesmo instante após uma indisponibilidade.
Consulte Retry com Backoff no Node.js.
Quando não reconectar?
- fechamento solicitado pelo usuário;
- credencial inválida;
- subprotocolo incompatível;
- limite de tentativas excedido;
- shutdown da aplicação;
- erro de configuração permanente.
Fila durante desconexão
Não acumule mensagens sem limite enquanto o socket está fechado. Defina:
- quantidade máxima;
- tamanho máximo;
- tempo de validade;
- tipos que podem ser descartados;
- operações que exigem confirmação.
bufferedAmount
if (socket.bufferedAmount > MAX_BUFFERED_BYTES) {
pauseProducer();
}A propriedade indica bytes enfileirados para envio. Ela não fornece uma Promise de drain, então o produtor precisa aplicar sua própria política.
Backpressure
A API WebSocket padrão não possui backpressure tão expressivo quanto streams. Enviar mais rápido que a rede pode consumir memória.
Mensagens grandes
Evite mensagens gigantes. Divida dados ou use HTTP para arquivos. WebSocket é mais adequado para eventos e mensagens de tamanho controlado.
Confirmação de entrega
send() não significa que o servidor processou a mensagem. Para operações importantes, inclua ID e resposta de confirmação:
{
"id": "op-123",
"type": "create-order",
"payload": {}
}Timeout de confirmação
Mantenha um Map limitado de operações pendentes e expire cada entrada. Remova tudo no close.
Idempotência
Após reconexão, uma mensagem pode ser reenviada. O servidor precisa reconhecer IDs já processados para evitar efeitos duplicados.
Ordem
WebSocket preserva ordem dos frames na conexão, mas reconexões e reenvios podem alterar a ordem lógica. Inclua sequência ou versão quando necessário.
Versionamento do protocolo
{
"version": 2,
"type": "order-updated",
"sequence": 18,
"payload": {}
}Rejeite mensagens de versão desconhecida de forma controlada.
Validação
Use schema para verificar:
- tipo;
- versão;
- campos obrigatórios;
- tamanho de strings;
- faixas numéricas;
- profundidade;
- quantidade de itens.
Compressão
Negociação de compressão depende do cliente e servidor. Mensagens comprimidas podem consumir CPU e expandir muito. Defina limites no lado servidor.
Proxy
Ambientes corporativos podem exigir proxy, recurso que a interface padrão pode não configurar diretamente. Uma biblioteca baseada em Undici ou WebSocket especializada pode ser necessária.
Certificados de cliente
mTLS e opções avançadas de conexão podem exigir um cliente com dispatcher ou configuração mais detalhada que a classe global.
Quando usar biblioteca?
Considere uma biblioteca quando precisa de:
- servidor WebSocket;
- headers customizados;
- proxy avançado;
- ping e pong de protocolo;
- controle de compressão;
- streams e backpressure específicos;
- compatibilidade com versões antigas do Node.js.
Quando usar a API nativa?
- cliente simples;
- Node.js moderno;
- interface compatível com navegador;
- texto e binário básicos;
- subprotocolos;
- menos dependências.
EventTarget
A classe usa addEventListener(). Guarde referências ou use AbortSignal para remover listeners.
AbortController para cleanup
const listeners = new AbortController();
socket.addEventListener(
'message',
handleMessage,
{ signal: listeners.signal }
);
listeners.abort();Fechar o socket e remover listeners são operações distintas.
Shutdown
async function shutdown() {
reconnectController.abort();
clearInterval(heartbeatTimer);
if (socket.readyState === WebSocket.OPEN) {
socket.close(1000, 'Application shutdown');
}
}Imponha prazo máximo para não bloquear o encerramento. Consulte Graceful Shutdown no Node.js.
Observabilidade
Registre:
- tentativas de conexão;
- tempo até open;
- close code;
- reconexões;
- mensagens recebidas e enviadas;
- bytes;
- bufferedAmount;
- timeouts de confirmação;
- erros de validação.
Não registre tokens nem payloads sensíveis.
Métricas
Acompanhe conexões ativas, duração, taxa de reconexão, latência do heartbeat e tamanho da fila.
Testando com servidor local
Suba um servidor controlado em porta aleatória e teste:
- open;
- mensagem de texto;
- binário;
- subprotocolo;
- close normal;
- queda abrupta;
- reconexão;
- fila limitada;
- heartbeat;
- mensagem inválida.
Esperando open em teste
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, {
once: true
});
socket.addEventListener('error', reject, {
once: true
});
});Adicione timeout para evitar teste pendurado.
Testando reconexão
Injete a função de delay e o construtor do socket. Assim, o teste controla tempo e conexões sem esperar segundos reais.
Testando backpressure
Simule bufferedAmount alto e confirme que o produtor pausa ou descarta mensagens de baixa prioridade.
Segurança
- use wss;
- valide mensagens;
- limite tamanho;
- autentique cedo;
- expire tokens;
- aplique rate limit;
- não confie apenas em Origin;
- trate duplicidade;
- restrinja subprotocolos;
- não registre credenciais.
Erros comuns
- Usar como servidor: a classe global é cliente.
- Enviar antes de open: readyState não permite.
- Fila ilimitada: memória cresce durante desconexão.
- Reconectar sem jitter: ocorre tempestade de conexões.
- Confiar em send: processamento remoto não foi confirmado.
- Sem heartbeat: conexão morta parece ativa.
- Usar ws em produção: tráfego fica sem TLS.
Boas práticas
- Use Node.js 22.4 ou superior.
- Trate a API como cliente.
- Aguarde open.
- Valide toda mensagem.
- Limite filas e tamanhos.
- Monitore bufferedAmount.
- Use heartbeat.
- Reconecte com backoff e jitter.
- Feche no shutdown.
- Use biblioteca quando precisar de recursos avançados.
Conclusão
O WebSocket Nativo no Node.js oferece um cliente estável e compatível com navegadores desde o Node.js 22.4.0. Ele atende conexões básicas com texto, binário, subprotocolos e eventos sem adicionar dependência.
A simplicidade não elimina responsabilidades de produção. A aplicação precisa autenticar, validar mensagens, limitar filas, implementar heartbeat, reconectar com jitter e fechar corretamente. Para servidor, proxy avançado, ping de protocolo ou controle detalhado, uma biblioteca especializada continua sendo a escolha adequada.



