Voltar para o blog
BunWebSocketTempo RealPerformance

Apuração em tempo real com Bun: o que a eleição ensina

28 de outubro de 2026·6 min de leitura·Diego Horvatti

No domingo, 25 de outubro, milhões de brasileiros passaram a noite atualizando a página da apuração do segundo turno até sair o resultado: Lula venceu quando os últimos votos foram contados, como mostrou a cobertura ao vivo da Reuters. Não vou falar de política aqui. Opinião sobre isso você já tem, e não vou ser eu a mudar. O que me interessa é outra coisa: o que acontece quando um país inteiro abre a mesma página ao mesmo tempo, e como você montaria um painel de apuração em tempo real com Bun sem ver o servidor cair no primeiro boletim.

Por que uma noite de eleição é um teste de carga de verdade

Quase todo sistema tem pico. O e-commerce tem a Black Friday. O app de ingresso tem a abertura de vendas. Só que a noite de eleição tem um formato bem específico.

O público é enorme e chega todo junto. Ninguém fica olhando um gráfico por educação: as pessoas ficam lá por ansiedade. E o dado muda a cada poucos segundos, mas é o mesmo dado para todo mundo.

Esse último ponto muda o jogo. Você não está servindo 1 milhão de páginas diferentes. Está servindo um único JSON, com poucos KB, para 1 milhão de pessoas.

Faz a conta do jeito ingênuo. Imagine 200 mil pessoas com o painel aberto, cada uma fazendo polling a cada 5 segundos. Dá 40 mil requisições por segundo. Se cada uma dessas requisições vai até o banco perguntar a mesma coisa, o problema não é o runtime: você construiu um ataque DDoS contra o seu próprio PostgreSQL.

Polling, SSE ou WebSocket: qual usar num painel ao vivo?

Antes de abrir o editor, vale escolher o transporte. Resumindo:

  • Polling com cache: o cliente pergunta de tempos em tempos e a CDN responde quase sempre. É simples, aguenta muito tráfego e passa por qualquer proxy corporativo.
  • SSE (Server-Sent Events): o servidor empurra atualizações por uma conexão HTTP aberta. É unidirecional, e é justamente disso que um placar precisa.
  • WebSocket: conexão bidirecional e persistente. É ótimo quando o cliente também fala, como em chat, jogo ou colaboração.

Num painel de apuração o cliente não fala nada, só escuta. Então, sendo honesto, polling com cache na CDN resolve 90% dos casos e é o que eu colocaria em produção primeiro.

Mesmo assim vale conhecer o caminho do WebSocket, porque é aí que o Bun tem um recurso que pouca gente usa: pub/sub embutido no servidor.

Como fazer apuração em tempo real com Bun e pub/sub nativo

No Node, quem quer WebSocket com broadcast normalmente instala uma lib, guarda as conexões num Set e faz um loop para mandar a mensagem para cada uma. Funciona, mas o loop roda em JavaScript e cresce junto com o número de clientes.

No Bun isso já vem no Bun.serve. Você inscreve a conexão num tópico e publica no tópico. O fan-out acontece no lado nativo do runtime.

let ultimoSnapshot = JSON.stringify({ secoesApuradas: 0 })

const server = Bun.serve({
  port: 3000,
  fetch(req, server) {
    if (server.upgrade(req)) return
    return new Response('Use WebSocket', { status: 426 })
  },
  websocket: {
    open(ws) {
      ws.subscribe('apuracao')
      ws.send(ultimoSnapshot)
    },
    message() {}
  }
})

setInterval(async () => {
  const parcial = await buscarParcial()
  ultimoSnapshot = JSON.stringify(parcial)
  server.publish('apuracao', ultimoSnapshot)
}, 2000)

Repara em três detalhes.

Primeiro: o banco é consultado uma vez a cada 2 segundos, não importa se tem 10 ou 100 mil pessoas conectadas. A consulta deixou de depender do número de usuários.

Segundo: quem entra no meio da noite recebe o último snapshot na hora, no open. Ninguém fica olhando uma tela vazia esperando o próximo ciclo.

Terceiro: são umas 20 linhas, sem nenhuma dependência. Esse é o argumento que mais me convence no Bun. Não é o benchmark do README. É eu conseguir ler o servidor inteiro sem rolar a tela.

O gargalo de um sistema ao vivo quase nunca é o runtime. É quantas vezes você pergunta a mesma coisa para o banco.

E se eu quiser só polling? O Bun ajuda também

Se você seguir meu conselho e começar pelo polling, o Bun continua útil. O segredo é deixar a CDN fazer o trabalho pesado com um cache curto:

Bun.serve({
  port: 3000,
  routes: {
    '/api/parcial': () =>
      new Response(ultimoSnapshot, {
        headers: {
          'Content-Type': 'application/json',
          'Cache-Control': 'public, max-age=2, s-maxage=2, stale-while-revalidate=5'
        }
      })
  }
})

Com s-maxage=2, a CDN segura a resposta por 2 segundos. Aquelas 40 mil requisições por segundo viram, na origem, algo perto de uma requisição a cada 2 segundos por ponto de presença. O stale-while-revalidate garante que ninguém espere enquanto a CDN busca a versão nova.

A rota só lê uma variável em memória, que um setInterval atualiza. O banco nem sabe que tem gente olhando.

O que costuma quebrar (e não é culpa do runtime)

Já vi painel ao vivo cair em projeto pequeno por motivos bem mais bobos que "o runtime é lento". Os clássicos:

  1. Reconexão em manada. O servidor reinicia e 50 mil clientes tentam reconectar no mesmo milissegundo. Coloque backoff com jitter no cliente: espere um tempo aleatório antes de tentar de novo.
  2. Payload gordo. Mandar o resultado de todos os municípios a cada 2 segundos quando a pessoa só olha o total nacional. Separe por tópico: apuracao:nacional, apuracao:sp, e por aí vai. O subscribe do Bun aceita quantos tópicos você quiser.
  3. Uma instância só. O pub/sub do Bun publica para quem está conectado naquele processo. Se você roda 4 instâncias atrás de um load balancer, cada uma precisa receber o dado novo. Dá para fazer isso com cada instância consultando a fonte por conta própria, ou com um Redis pub/sub distribuindo o evento.
  4. Timeout de proxy. Alguns proxies derrubam conexão ociosa depois de 60 segundos. O Bun tem idleTimeout e manda ping sozinho, mas confira o que tem na frente dele.

O item 3 pega muita gente. Pub/sub nativo resolve o fan-out dentro de uma máquina. Ele não é um broker distribuído, e tratar ele como se fosse é pedir para metade dos usuários ver um número desatualizado.

Vale trocar seu backend para Bun por causa disso?

Minha opinião: não troque nada por causa de uma noite de eleição. Agora, se você vai começar um serviço novo que é basicamente "empurrar o mesmo dado para muita gente", o Bun é uma escolha muito boa. O que pesa é ter WebSocket, pub/sub, roteamento e TypeScript sem nenhum npm install. É menos coisa para atualizar e menos coisa para dar errado às onze da noite de um domingo.

Agora, se o seu app em Node já aguenta o tranco com cache bem configurado, o ganho de migrar é pequeno perto do risco. Arquitetura vence runtime. Um Express atrás de uma CDN bem configurada aguenta mais que um Bun consultando o banco a cada requisição.

A lição que fica da apuração é simples: quando todo mundo quer a mesma resposta, calcule uma vez e distribua muitas. O resto é detalhe de implementação, e o Bun deixa esse detalhe bem curto.

Se quiser ver como eu aplico essas ideias em sistemas reais, dá uma olhada nos projetos que já entreguei.

Resumo para o LinkedIn

Meia-noite, um país inteiro apertando F5 na mesma página. E o servidor de pé.

Uma noite de eleição não é só tráfego alto. É um milhão de pessoas pedindo o mesmo JSON de poucos KB ao mesmo tempo.

Com polling ingênuo, 200 mil painéis abertos viram 40 mil requisições por segundo contra o seu próprio banco.

O jeito certo é consultar a fonte uma vez a cada 2 segundos e distribuir para todo mundo. No Bun isso cabe em umas 20 linhas, com pub/sub nativo e zero dependências.

Arquitetura vence runtime: quando todo mundo quer a mesma resposta, calcule uma vez e distribua muitas.

Escrevi o passo a passo completo, com WebSocket, cache na CDN e as armadilhas que derrubam painel ao vivo. O link está nos comentários.

#Bun #TypeScript #WebSocket #Performance #Backend