Volver al blog
BunWebSocketTiempo RealRendimiento

Escrutinio en tiempo real con Bun: lo que enseña la elección

28 de octubre de 2026·6 min de lectura·Diego Horvatti

El domingo 25 de octubre, millones de brasileños pasaron la noche recargando la página del escrutinio de la segunda vuelta hasta que salió el resultado. Lula ganó cuando se contaron los últimos votos, como mostró la cobertura en vivo de Reuters. No voy a hablar de política aquí. Tu opinión sobre eso ya la tienes, y no voy a ser yo quien la cambie. Lo que me interesa es otra cosa: qué pasa cuando todo un país abre la misma página al mismo tiempo, y cómo montarías un panel de escrutinio en tiempo real con Bun sin ver caer el servidor en el primer boletín.

Por qué una noche electoral es una prueba de carga de verdad

Casi todo sistema tiene picos. El e-commerce tiene el Black Friday. La app de entradas tiene la apertura de ventas. Pero la noche electoral tiene una forma muy particular.

El público es enorme y llega todo junto. Nadie mira un gráfico por cortesía: la gente se queda ahí por ansiedad. Y el dato cambia cada pocos segundos, pero es el mismo dato para todos.

Ese último punto cambia el juego. No estás sirviendo 1 millón de páginas distintas. Estás sirviendo un único JSON, de pocos KB, a 1 millón de personas.

Haz la cuenta a lo ingenuo. Imagina 200 mil personas con el panel abierto, cada una haciendo polling cada 5 segundos. Son 40 mil peticiones por segundo. Si cada una de esas peticiones va hasta la base de datos a preguntar lo mismo, el problema no es el runtime: construiste un ataque DDoS contra tu propio PostgreSQL.

Polling, SSE o WebSocket: ¿cuál usar en un panel en vivo?

Antes de abrir el editor, conviene elegir el transporte. En resumen:

  • Polling con caché: el cliente pregunta cada cierto tiempo y la CDN responde casi siempre. Es simple, aguanta mucho tráfico y pasa por cualquier proxy corporativo.
  • SSE (Server-Sent Events): el servidor empuja actualizaciones por una conexión HTTP abierta. Es unidireccional, y eso es justo lo que necesita un marcador.
  • WebSocket: conexión bidireccional y persistente. Es ideal cuando el cliente también habla, como en un chat, un juego o una herramienta colaborativa.

En un panel de escrutinio el cliente no dice nada, solo escucha. Así que, siendo honesto, polling con caché en la CDN resuelve el 90% de los casos y es lo que yo pondría en producción primero.

Aun así vale la pena conocer el camino del WebSocket, porque ahí Bun tiene un recurso que poca gente usa: pub/sub integrado en el servidor.

Cómo hacer un escrutinio en tiempo real con Bun y pub/sub nativo

En Node, quien quiere WebSocket con broadcast normalmente instala una librería, guarda las conexiones en un Set y hace un bucle para mandar el mensaje a cada una. Funciona, pero el bucle corre en JavaScript y crece junto con el número de clientes.

En Bun eso ya viene en Bun.serve. Suscribes la conexión a un tópico y publicas en el tópico. El fan-out ocurre en el lado nativo del 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)

Fíjate en tres detalles.

Primero: la base de datos se consulta una vez cada 2 segundos, da igual si hay 10 o 100 mil personas conectadas. La consulta dejó de depender del número de usuarios.

Segundo: quien entra a mitad de la noche recibe el último snapshot al instante, en el open. Nadie se queda mirando una pantalla vacía esperando el siguiente ciclo.

Tercero: son unas 20 líneas, sin ninguna dependencia. Ese es el argumento que más me convence de Bun. No es el benchmark del README. Es poder leer el servidor entero sin hacer scroll.

El cuello de botella de un sistema en vivo casi nunca es el runtime. Es cuántas veces le preguntas lo mismo a la base de datos.

¿Y si solo quiero polling? Bun también ayuda

Si sigues mi consejo y empiezas por el polling, Bun sigue siendo útil. El secreto es dejar que la CDN haga el trabajo pesado con una caché corta:

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'
        }
      })
  }
})

Con s-maxage=2, la CDN guarda la respuesta durante 2 segundos. Esas 40 mil peticiones por segundo se convierten, en el origen, en algo cercano a una petición cada 2 segundos por punto de presencia. El stale-while-revalidate garantiza que nadie espere mientras la CDN busca la versión nueva.

La ruta solo lee una variable en memoria, que un setInterval actualiza. La base de datos ni se entera de que hay gente mirando.

Lo que suele romperse (y no es culpa del runtime)

He visto paneles en vivo caerse en proyectos pequeños por motivos mucho más tontos que "el runtime es lento". Los clásicos:

  1. Reconexión en manada. El servidor se reinicia y 50 mil clientes intentan reconectarse en el mismo milisegundo. Pon backoff con jitter en el cliente: espera un tiempo aleatorio antes de volver a intentarlo.
  2. Payload pesado. Mandar el resultado de todos los municipios cada 2 segundos cuando la persona solo mira el total nacional. Separa por tópico: apuracao:nacional, apuracao:sp, y así sucesivamente. El subscribe de Bun acepta todos los tópicos que quieras.
  3. Una sola instancia. El pub/sub de Bun publica para quien está conectado en ese proceso. Si corres 4 instancias detrás de un load balancer, cada una necesita recibir el dato nuevo. Puedes resolverlo con cada instancia consultando la fuente por su cuenta, o con un Redis pub/sub distribuyendo el evento.
  4. Timeout del proxy. Algunos proxies cortan las conexiones inactivas después de 60 segundos. Bun tiene idleTimeout y manda ping solo, pero revisa qué tiene delante.

El punto 3 atrapa a mucha gente. El pub/sub nativo resuelve el fan-out dentro de una máquina. No es un broker distribuido, y tratarlo como si lo fuera es buscar que la mitad de los usuarios vea un número desactualizado.

¿Vale la pena cambiar tu backend a Bun por esto?

Mi opinión: no cambies nada por una noche electoral. Ahora bien, si vas a empezar un servicio nuevo que básicamente consiste en "empujar el mismo dato a mucha gente", Bun es una muy buena elección. Lo que pesa es tener WebSocket, pub/sub, enrutamiento y TypeScript sin ningún npm install. Es menos cosa que actualizar y menos cosa que puede fallar a las once de la noche de un domingo.

Pero si tu app en Node ya aguanta el golpe con una caché bien configurada, la ganancia de migrar es pequeña frente al riesgo. La arquitectura le gana al runtime. Un Express detrás de una CDN bien configurada aguanta más que un Bun consultando la base de datos en cada petición.

La lección que deja el escrutinio es simple: cuando todos quieren la misma respuesta, calcula una vez y distribuye muchas. Lo demás es detalle de implementación, y Bun deja ese detalle bien corto.

Si quieres ver cómo aplico estas ideas en sistemas reales, échale un vistazo a los proyectos que ya entregué.

Resumen para LinkedIn

Medianoche, todo un país apretando F5 en la misma página. Y el servidor de pie.

Una noche electoral no es solo tráfico alto. Es un millón de personas pidiendo el mismo JSON de pocos KB al mismo tiempo.

Con un polling ingenuo, 200 mil paneles abiertos se convierten en 40 mil peticiones por segundo contra tu propia base de datos.

Lo correcto es consultar la fuente una vez cada 2 segundos y distribuir a todos. En Bun eso cabe en unas 20 líneas, con pub/sub nativo y cero dependencias.

La arquitectura le gana al runtime: cuando todos quieren la misma respuesta, calcula una vez y distribuye muchas.

Escribí el paso a paso completo, con WebSocket, caché en la CDN y las trampas que tumban un panel en vivo. El enlace está en los comentarios.

#Bun #TypeScript #WebSocket #Rendimiento #Backend