Cloud e plataforma

WebSocket vs Server-Sent Events: Escolhendo a Tecnologia Certa para Tempo Real em Produção

WebSocket e SSE resolvem comunicação em tempo real, mas não são intercambiáveis. Entender as diferenças arquiteturais previne gargalos de performance e complexidade operacional.

10/03/20267 min de leituraCloud
WebSocket vs Server-Sent Events: Escolhendo a Tecnologia Certa para Tempo Real em Produção

Resumo executivo

WebSocket e SSE resolvem comunicação em tempo real, mas não são intercambiáveis. Entender as diferenças arquiteturais previne gargalos de performance e complexidade operacional.

Ultima atualizacao: 10/03/2026

Resumo executivo

Funcionalidade em tempo real—atualizações ao vivo, notificações push, edição colaborativa—evoluiu de "bom de se ter" para pré-requisito para aplicações web modernas. As duas abordagens dominantes para comunicação em tempo real de navegador para servidor são WebSocket e Server-Sent Events (SSE).

A decisão de engenharia não é apenas técnica; é arquitetural. WebSocket fornece comunicação full-duplex, enquanto SSE é push unidirecional de servidor para cliente. Ambos têm características distintas de escala, requisitos operacionais e modos de falha. Escolher a tecnologia errada leva a complexidade de infraestrutura desnecessária, vulnerabilidades de segurança ou má experiência do usuário.

Modelos de conexão: Entendendo a diferença fundamental

WebSocket: Comunicação bidirecional full-duplex

WebSocket atualiza uma conexão HTTP para um socket TCP persistente que permite que os dados fluam em ambas as direções simultaneamente.

typescript// Implementação de cliente WebSocket
class RealtimeChatClient {
  private ws: WebSocket;
  private tentativasReconexao = 0;
  private readonly MAX_TENTATIVAS_RECONEXAO = 5;
  private readonly DELAY_RECONEXAO_MS = 3000;

  constructor(private url: string) {
    this.ws = this.conectar();
  }

  private conectar(): WebSocket {
    const ws = new WebSocket(this.url);

    ws.onopen = () => {
      console.log('WebSocket conectado');
      this.tentativasReconexao = 0;
      // Enviar autenticação
      this.enviarMensagem({
        tipo: 'auth',
        token: this.obterTokenAutenticacao()
      });
    };

    ws.onmessage = (event) => {
      const mensagem = JSON.parse(event.data);
      this.tratarMensagem(mensagem);
    };

    ws.onerror = (erro) => {
      console.error('Erro no WebSocket:', erro);
    };

    ws.onclose = (event) => {
      console.log('WebSocket fechado:', event.code, event.reason);
      if (!event.wasClean && this.tentativasReconexao < this.MAX_TENTATIVAS_RECONEXAO) {
        this.tentativasReconexao++;
        setTimeout(() => {
          this.ws = this.conectar();
        }, this.DELAY_RECONEXAO_MS);
      }
    };

    return ws;
  }

  enviarMensagem(mensagem: any) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(mensagem));
    } else {
      console.error('WebSocket não está aberto');
    }
  }

  private tratarMensagem(mensagem: any) {
    switch (mensagem.tipo) {
      case 'chat_message':
        this.exibirMensagem(mensagem.dados);
        break;
      case 'presence_update':
        this.atualizarPresenca(mensagem.dados);
        break;
      case 'typing_indicator':
        this.mostrarIndicadorDigitacao(mensagem.dados);
        break;
    }
  }

  private obterTokenAutenticacao(): string {
    return localStorage.getItem('auth_token') || '';
  }

  private exibirMensagem(dados: any) {
    // Atualizar UI com nova mensagem
  }

  private atualizarPresenca(dados: any) {
    // Atualizar indicadores de presença
  }

  private mostrarIndicadorDigitacao(dados: any) {
    // Mostrar/ocultar indicador de digitação
  }
}

Server-Sent Events: Push unidirecional de servidor para cliente

SSE usa uma conexão HTTP de longa duração onde o servidor envia eventos para o cliente. O cliente não pode enviar dados de volta através da conexão SSE.

typescript// Implementação de cliente SSE
class LiveUpdatesClient {
  private eventSource: EventSource;
  private tentativasReconexao = 0;
  private readonly MAX_TENTATIVAS_RECONEXAO = 5;

  constructor(private url: string) {
    this.eventSource = this.conectar();
  }

  private conectar(): EventSource {
    const eventSource = new EventSource(this.url);

    eventSource.onopen = () => {
      console.log('Conexão SSE estabelecida');
      this.tentativasReconexao = 0;
    };

    eventSource.onmessage = (event) => {
      const dados = JSON.parse(event.data);
      this.tratarAtualizacao(dados);
    };

    eventSource.onerror = (erro) => {
      console.error('Erro no SSE:', erro);
      this.eventSource.close();

      if (this.tentativasReconexao < this.MAX_TENTATIVAS_RECONEXAO) {
        this.tentativasReconexao++;
        setTimeout(() => {
          this.eventSource = this.conectar();
        }, 3000);
      }
    };

    return eventSource;
  }

  private tratarAtualizacao(dados: any) {
    switch (dados.tipo) {
      case 'price_update':
        this.atualizarPreco(dados);
        break;
      case 'notification':
        this.mostrarNotificacao(dados);
        break;
      case 'system_status':
        this.atualizarStatus(dados);
        break;
    }
  }

  private atualizarPreco(dados: any) {
    // Atualizar preço na UI
  }

  private mostrarNotificacao(dados: any) {
    // Exibir notificação toast
  }

  private atualizarStatus(dados: any) {
    // Atualizar indicador de status do sistema
  }

  // Nota: Não é possível enviar dados de volta através da conexão SSE
  // Use fetch/HTTP para comunicação cliente-servidor
}

Matriz de comparação: Quando usar qual

CritérioWebSocketSSE (Server-Sent Events)
DirecionalidadeBidirecional (servidor ↔ cliente)Unidirecional (servidor → cliente apenas)
Formato de dadosBinário ou textoTexto apenas (tipicamente JSON)
Suporte de navegadorExcelente (todos os navegadores modernos)Excelente (todos os navegadores modernos)
Compatibilidade de proxyPode requerer configuração especialFunciona através da maioria dos proxies
ReconexãoImplementação manual necessáriaAutomática com backoff exponencial
Tipos de eventoTipos de mensagem customizadosCanais de evento nomeados
Overhead de conexãoHandshake HTTP inicial + upgradeConexão HTTP persistente
Complexidade de escalaAlta (pool de conexões, gerenciamento de estado)Moderada (stateless mais fácil)
Adequação de caso de usoChat, jogos, edição colaborativaAtualizações ao vivo, notificações, tickers de ações

Análise de casos de uso

Quando WebSocket é a escolha certa

1. Aplicações de chat em tempo real

Chat requer comunicação bidirecional: usuários enviam mensagens e recebem mensagens instantaneamente. A natureza full-duplex do WebSocket é ideal.

typescript// Aplicação de chat usando WebSocket
class AplicacaoChat {
  private ws: WebSocket;
  private usuarioAtual: Usuario;

  async enviarMensagem(conteudo: string) {
    this.ws.send(JSON.stringify({
      tipo: 'send_message',
      dados: {
        userId: this.usuarioAtual.id,
        conteudo,
        timestamp: Date.now()
      }
    }));
  }

  private tratarMensagemRecebida(mensagem: ChatMessage) {
    // Exibir mensagem recebida
    // Atualizar contador de não lidas
    // Disparar notificação se necessário
  }

  async definirStatusDigitacao(estaDigitando: boolean) {
    this.ws.send(JSON.stringify({
      tipo: 'typing_status',
      dados: {
        userId: this.usuarioAtual.id,
        estaDigitando,
        timestamp: Date.now()
      }
    }));
  }

  private tratarStatusDigitacao(status: TypingStatus) {
    // Mostrar/ocultar indicador de digitação
  }
}

2. Jogos multiplayer

Jogos requerem comunicação bidirecional frequente, com baixa latência e múltiplos eventos simultâneos.

3. Edição colaborativa

Ferramentas como Google Docs ou Figma requerem sincronização em tempo real das ações do usuário em todos os participantes.

Quando SSE é a escolha certa

1. Atualizações de dados financeiros ao vivo

Preços de ações, valores de criptomoedas ou quaisquer tickers financeiros precisam apenas de push de servidor para cliente.

typescript// Ticker de ações usando SSE
class StockTickerClient {
  private eventSource: EventSource;
  private simbolosInscritos: Set<string> = new Set();

  async inscrever(simbolos: string[]) {
    // Use HTTP POST para se inscrever (não SSE)
    await fetch('/api/acoes/inscrever', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ simbolos })
    });

    simbolos.forEach(simbolo => this.simbolosInscritos.add(simbolo));
  }

  async desinscrever(simbolos: string[]) {
    await fetch('/api/acoes/desinscrever', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ simbolos })
    });

    simbolos.forEach(simbolo => this.simbolosInscritos.delete(simbolo));
  }

  private tratarAtualizacaoPreco(atualizacao: PriceUpdate) {
    if (this.simbolosInscritos.has(atualizacao.simbolo)) {
      this.atualizarDisplay(atualizacao);
    }
  }

  private atualizarDisplay(atualizacao: PriceUpdate) {
    // Atualizar preço na UI
    // Destacar mudança de preço (verde/vermelho)
    // Atualizar gráfico
  }
}

2. Notificações em tempo real

Notificações push, alertas ou atualizações de status do sistema fluem apenas do servidor para o cliente.

3. Monitoramento de dashboard ao vivo

Dashboards operacionais exibindo métricas, logs ou status do sistema precisam apenas de atualizações de servidor para cliente.

Considerações de escala

Desafios de escala de WebSocket

Conexões WebSocket são com estado e persistentes, o que cria complexidade de escala:

1. Pool de conexões e gerenciamento de estado

typescript// Servidor WebSocket com pool de conexões
class WebSocketServer {
  private conexoes: Map<string, WebSocket> = new Map();
  private usuarioSalas: Map<string, Set<string>> = new Map(); // userId -> roomIds

  handleConnection(userId: string, ws: WebSocket) {
    this.conexoes.set(userId, ws);

    ws.on('message', (message) => {
      const dados = JSON.parse(message);
      this.tratarMensagem(userId, dados);
    });

    ws.on('close', () => {
      this.conexoes.delete(userId);
      this.removerUsuarioDeTodasSalas(userId);
    });
  }

  broadcastParaSala(salaId: string, mensagem: any) {
    const usuariosNaSala = this.obterUserIdsNaSala(salaId);
    usuariosNaSala.forEach(userId => {
      const ws = this.conexoes.get(userId);
      if (ws && ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(mensagem));
      }
    });
  }

  private obterUserIdsNaSala(salaId: string): string[] {
    // Retornar todos os user IDs na sala
    return Array.from(this.usuarioSalas.entries())
      .filter(([_, salas]) => salas.has(salaId))
      .map(([userId, _]) => userId);
  }

  private removerUsuarioDeTodasSalas(userId: string) {
    const salas = this.usuarioSalas.get(userId);
    if (salas) {
      salas.forEach(salaId => this.sairDaSala(userId, salaId));
    }
    this.usuarioSalas.delete(userId);
  }
}

2. Compatibilidade de balanceador de carga

Conexões WebSocket requerem balanceadores de carga que suportam sessões sticky ou implementam roteamento consciente de WebSocket.

3. Escala horizontal através de múltiplos servidores

Quando conexões WebSocket são distribuídas através de múltiplos servidores, comunicação entre servidores é necessária para broadcast:

typescript// Pub/sub Redis para broadcast WebSocket cross-server
class DistributedWebSocketServer {
  private conexoes: Map<string, WebSocket> = new Map();
  private redisPublisher: Redis;
  private redisSubscriber: Redis;

  constructor(private serverId: string) {
    this.redisSubscriber.subscribe('websocket:broadcast');
    this.redisSubscriber.on('message', (channel, message) => {
      this.tratarMensagemBroadcast(JSON.parse(message));
    });
  }

  async broadcastParaTodos(mensagem: any) {
    // Publicar no Redis para todos os servidores receberem
    await this.redisPublisher.publish('websocket:broadcast', JSON.stringify({
      serverId: this.serverId,
      mensagem
    }));
  }

  private tratarMensagemBroadcast(dados: any) {
    // Não rebroadcast se mensagem originou deste servidor
    if (dados.serverId === this.serverId) {
      return;
    }

    // Enviar para todos os clientes conectados neste servidor
    this.conexoes.forEach(ws => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(dados.mensagem));
      }
    });
  }
}

Vantagens de escala de SSE

SSE é mais fácil de escalar porque:

1. Design de servidor sem estado

Servidores SSE podem ser sem estado e escalados horizontalmente atrás de balanceadores de carga padrão.

2. Reconexão integrada

A API EventSource gerencia automaticamente reconexão com backoff exponencial, reduzindo complexidade do lado do servidor.

3. Escala horizontal simples

Múltiplos servidores SSE podem servir clientes diferentes sem necessidade de comunicação entre servidores para broadcast.

typescript// Servidor SSE sem estado
class SSEServer {
  private clientes: Map<http.ServerResponse, Set<string>> = new Map();

  handleSSEConnection(req: http.IncomingMessage, res: http.ServerResponse) {
    // Definir headers SSE
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no' // Desabilitar buffering do nginx
    });

    // Rastrear cliente e suas inscrições
    this.clientes.set(res, new Set());

    // Enviar mensagem de conexão inicial
    this.enviarEvento(res, { tipo: 'connected', timestamp: Date.now() });

    req.on('close', () => {
      this.clientes.delete(res);
    });
  }

  enviarEvento(res: http.ServerResponse, dados: any) {
    const evento = `data: ${JSON.stringify(dados)}\n\n`;
    res.write(evento);
  }

  broadcastParaTodos(mensagem: any) {
    this.clientes.forEach((_, res) => {
      this.enviarEvento(res, mensagem);
    });
  }
}

Considerações de segurança

Segurança de WebSocket

1. Autenticação durante handshake

typescript// Middleware de autenticação WebSocket
const authenticateWebSocket = async (req: http.IncomingRequest): Promise<User | null> => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return null;
  }

  try {
    const decoded = await verifyJWT(token);
    return await User.findById(decoded.userId);
  } catch (error) {
    return null;
  }
};

// Servidor WebSocket com autenticação
wss.on('connection', async (ws: WebSocket, req: http.IncomingRequest) => {
  const usuario = await authenticateWebSocket(req);

  if (!usuario) {
    ws.close(4008, 'Unauthorized');
    return;
  }

  // Armazenar conexão de usuário
  connections.set(usuario.id, ws);

  ws.on('message', (message) => {
    tratarMensagemUsuario(usuario.id, message);
  });
});

2. Rate limiting e prevenção de abuso

Conexões WebSocket podem ser abusadas para ataques DoS ou exaustão de recursos.

typescript// Rate limiting para mensagens WebSocket
class WebSocketRateLimiter {
  private contagemMensagens: Map<string, number[]> = new Map();
  private readonly JANELA_MS = 60000; // 1 minuto
  private readonly MAX_MENSAGENS = 100;

  verificarRateLimit(userId: string): boolean {
    const agora = Date.now();
    const mensagensUsuario = this.contagemMensagens.get(userId) || [];

    // Remover mensagens fora da janela de tempo
    const mensagensRecentes = mensagensUsuario.filter(timestamp => agora - timestamp < this.JANELA_MS);

    if (mensagensRecentes.length >= this.MAX_MENSAGENS) {
      return false;
    }

    mensagensRecentes.push(agora);
    this.contagemMensagens.set(userId, mensagensRecentes);
    return true;
  }
}

Segurança de SSE

1. Autenticação através de parâmetros de query ou cookies

typescript// Servidor SSE com autenticação
const handleSSEConnection = async (req: http.IncomingRequest, res: http.ServerResponse) => {
  // Extrair token de cookie ou parâmetro de query
  const token = req.headers.cookie?.match(/auth_token=([^;]+)/)?.[1]
            || new URL(req.url || '', 'http://localhost').searchParams.get('token');

  if (!token) {
    res.writeHead(401);
    res.end();
    return;
  }

  const usuario = await autenticarToken(token);
  if (!usuario) {
    res.writeHead(401);
    res.end();
    return;
  }

  // Estabelecer conexão SSE
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  // Enviar evento inicial
  res.write(`data: ${JSON.stringify({ tipo: 'connected', userId: usuario.id })}\n\n`);

  // Continuar enviando eventos...
};

2. Autorização e filtragem baseada em escopo

Clientes devem receber apenas eventos que são autorizados a ver.

Considerações de performance e recursos

Uso de recursos de WebSocket

Overhead de conexão:

  • Cada conexão WebSocket mantém um socket TCP aberto
  • Memória do servidor: ~1-2KB por conexão (estado de conexão)
  • CPU do servidor: mínima para conexões ociosas
  • Rede: pacotes keep-alive (tipicamente a cada 30-60 segundos)

Exemplo de benchmark:

typescript// Teste de estresse de conexões WebSocket
async function benchmarkWebSocketConnections() {
  const CONEXOES_SIMULTANEAS = 10000;
  const clientes: WebSocket[] = [];

  console.time('connect');
  for (let i = 0; i < CONEXOES_SIMULTANEAS; i++) {
    const ws = new WebSocket('ws://localhost:8080');
    clientes.push(ws);
    await new Promise(resolve => ws.onopen = resolve);
  }
  console.timeEnd('connect');

  // Medir uso de memória
  const usoMemoria = process.memoryUsage();
  console.log('Uso de memória:', {
    heapUsed: `${Math.round(usoMemoria.heapUsed / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(usoMemoria.heapTotal / 1024 / 1024)} MB`
  });
}

Uso de recursos de SSE

Overhead de conexão:

  • Cada conexão SSE mantém uma conexão HTTP aberta
  • Memória do servidor: ~500B-1KB por conexão (menos que WebSocket)
  • CPU do servidor: mínima para conexões ociosas
  • Rede: pacotes keep-alive (tipicamente a cada 30-60 segundos)

Vantagem de eficiência de memória: Conexões SSE são tipicamente mais leves em memória porque não mantêm tanto estado quanto conexões WebSocket.

Abordagens híbridas

Em muitos sistemas de produção, a solução ótima combina ambas as tecnologias baseadas no caso de uso:

typescript// Cliente tempo real híbrido
class HybridRealtimeClient {
  private clienteSSE: LiveUpdatesClient;
  private clienteWS: RealtimeChatClient;
  private wsConectado = false;

  constructor(private config: HybridConfig) {
    // Inicializar SSE para atualizações servidor-para-cliente
    this.clienteSSE = new LiveUpdatesClient(config.sseUrl);

    // Inicializar WebSocket para comunicação bidirecional
    this.clienteWS = new RealtimeChatClient(config.wsUrl);

    this.clienteWS.onConnectionChange = (conectado: boolean) => {
      this.wsConectado = conectado;
    };
  }

  enviarMensagemChat(mensagem: string) {
    // Usar WebSocket para enviar mensagens
    if (this.wsConectado) {
      this.clienteWS.enviarMensagem(mensagem);
    } else {
      console.error('WebSocket não conectado');
      // Fallback: usar HTTP POST
      this.postarMensagemViaHTTP(mensagem);
    }
  }

  private async postarMensagemViaHTTP(mensagem: string) {
    await fetch('/api/chat/mensagens', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ conteudo: mensagem })
    });
  }

  inscreverAtualizacoesAoVivo() {
    // Usar SSE para receber atualizações
    this.clienteSSE.conectar();
  }
}

Framework de decisão

Use este framework para escolher a tecnologia certa para seu caso de uso:

Perguntas a fazer

  1. O cliente precisa enviar dados para o servidor em tempo real?
  • Sim → WebSocket
  • Não → Considere SSE
  1. Baixa latência é crítica para ambas as direções?
  • Sim → WebSocket
  • Não → SSE pode ser suficiente
  1. Quantas conexões simultâneas você espera?
  • < 10.000 → Ambas opções viáveis
  • 10.000-100.000 → SSE pode escalar mais fácil
  • > 100.000 → Requer infraestrutura WebSocket especializada
  1. Qual é a expertise operacional da sua equipe?
  • Experienciada com infraestrutura WebSocket → WebSocket
  • Preferindo arquitetura sem estado mais simples → SSE
  1. Você precisa de transferência de dados binários?
  • Sim → WebSocket
  • Não → SSE suficiente

Checklist de implantação em produção

Para WebSocket:

  • [ ] Implementar pool de conexões e gerenciamento de estado
  • [ ] Configurar balanceadores de carga para sessões sticky
  • [ ] Implementar comunicação cross-server para broadcast
  • [ ] Adicionar rate limiting e prevenção de abuso
  • [ ] Configurar monitoramento de saúde de conexão
  • [ ] Implementar reconexão graceful do lado do cliente
  • [ ] Adicionar tratamento de timeout de conexão

Para SSE:

  • [ ] Implementar autenticação via cookies ou parâmetros de query
  • [ ] Definir headers SSE apropriados (Content-Type, Cache-Control)
  • [ ] Configurar proxies reversos para desabilitar buffering
  • [ ] Adicionar autorização e filtragem baseada em escopo
  • [ ] Implementar replay de eventos para clientes desconectados
  • [ ] Configurar monitoramento de saúde de conexão

Conclusão

WebSocket e SSE são tecnologias poderosas para comunicação em tempo real, mas resolvem problemas diferentes. WebSocket se destaca em comunicação bidirecional, com baixa latência onde cliente e servidor precisam trocar dados frequentemente. SSE brilha em cenários unidirecionais servidor-para-cliente onde simplicidade e escala sem estado são prioridades.

A decisão não é apenas sobre capacidades técnicas—é sobre complexidade operacional, expertise da equipe e manutenibilidade a longo prazo. Escolha a tecnologia que corresponde ao seu caso de uso, requisitos de escala e capacidades operacionais.


Construindo uma aplicação em tempo real e precisa de orientação sobre decisões arquiteturais? Fale com a Imperialis sobre projetar e implementar a solução tempo real certa para seu sistema de produção.

Fontes

Leituras relacionadas