Back to the blog
BunWebSocketReal TimePerformance

Live election results with Bun: what election night teaches

October 28, 2026·6 min read·Diego Horvatti

On Sunday, October 25, millions of Brazilians spent the night refreshing the runoff results page until the outcome came in. Lula won once the last votes were counted, as shown in Reuters' live coverage. I'm not going to talk politics here. You already have your opinion on that, and I won't be the one to change it. What interests me is something else: what happens when a whole country opens the same page at the same time, and how you'd build a live election results dashboard with Bun without watching the server go down at the first update.

Why election night is a real load test

Almost every system has a peak. E-commerce has Black Friday. Ticketing apps have the moment sales open. But election night has a very specific shape.

The audience is huge and arrives all at once. Nobody watches a chart out of politeness. People stay there out of anxiety. And the data changes every few seconds, but it's the same data for everyone.

That last point changes the game. You're not serving 1 million different pages. You're serving a single JSON, a few KB in size, to 1 million people.

Do the math the naive way. Picture 200,000 people with the dashboard open, each one polling every 5 seconds. That's 40,000 requests per second. If every one of those requests goes to the database to ask the same thing, the problem isn't the runtime. You've built a DDoS attack against your own PostgreSQL.

Polling, SSE or WebSocket: which one for a live dashboard?

Before opening the editor, pick the transport. In short:

  • Polling with caching: the client asks every so often and the CDN answers almost every time. It's simple, handles a lot of traffic and gets through any corporate proxy.
  • SSE (Server-Sent Events): the server pushes updates over an open HTTP connection. It's one-way, and that's exactly what a scoreboard needs.
  • WebSocket: a persistent, two-way connection. It's great when the client also talks, like in chat, games or collaboration.

On an election dashboard the client says nothing. It only listens. So, to be honest, polling with CDN caching covers 90% of cases and it's what I'd put in production first.

Still, the WebSocket path is worth knowing, because that's where Bun has a feature few people use: pub/sub built into the server.

How to build live election results with Bun and native pub/sub

In Node, if you want WebSocket with broadcast, you usually install a library, keep the connections in a Set and loop over them to send the message to each one. It works, but the loop runs in JavaScript and grows with the number of clients.

In Bun this already ships with Bun.serve. You subscribe the connection to a topic and publish to the topic. The fan-out happens on the native side of the runtime.

let lastSnapshot = JSON.stringify({ sectionsCounted: 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('results')
      ws.send(lastSnapshot)
    },
    message() {}
  }
})

setInterval(async () => {
  const partial = await fetchPartial()
  lastSnapshot = JSON.stringify(partial)
  server.publish('results', lastSnapshot)
}, 2000)

Notice three details.

First: the database is queried once every 2 seconds, whether 10 or 100,000 people are connected. The query no longer depends on the number of users.

Second: anyone who joins in the middle of the night gets the latest snapshot right away, in open. Nobody stares at an empty screen waiting for the next cycle.

Third: it's about 20 lines, with zero dependencies. That's the argument that convinces me most about Bun. It's not the benchmark in the README. It's being able to read the whole server without scrolling.

The bottleneck of a live system is almost never the runtime. It's how many times you ask the database the same question.

What if I just want polling? Bun helps there too

If you take my advice and start with polling, Bun is still useful. The trick is to let the CDN do the heavy lifting with a short cache:

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

With s-maxage=2, the CDN holds the response for 2 seconds. Those 40,000 requests per second become, at the origin, something close to one request every 2 seconds per point of presence. stale-while-revalidate makes sure nobody waits while the CDN fetches the new version.

The route only reads an in-memory variable that a setInterval keeps updated. The database doesn't even know anyone is watching.

What usually breaks (and it's not the runtime's fault)

I've seen live dashboards fall over in small projects for reasons much sillier than "the runtime is slow". The classics:

  1. Thundering herd on reconnect. The server restarts and 50,000 clients try to reconnect in the same millisecond. Add backoff with jitter on the client: wait a random amount of time before trying again.
  2. Fat payload. Sending results for every city every 2 seconds when the user only looks at the national total. Split by topic: results:national, results:sp, and so on. Bun's subscribe accepts as many topics as you want.
  3. A single instance. Bun's pub/sub publishes to whoever is connected to that process. If you run 4 instances behind a load balancer, each one needs to get the new data. You can do that by having each instance query the source on its own, or with Redis pub/sub distributing the event.
  4. Proxy timeout. Some proxies drop idle connections after 60 seconds. Bun has idleTimeout and sends pings on its own, but check what sits in front of it.

Item 3 catches a lot of people. Native pub/sub handles fan-out inside one machine. It's not a distributed broker, and treating it like one is asking for half your users to see a stale number.

Is it worth switching your backend to Bun because of this?

My take: don't switch anything because of one election night. But if you're starting a new service that's basically "push the same data to a lot of people", Bun is a very good choice. What matters is getting WebSocket, pub/sub, routing and TypeScript without a single npm install. Fewer things to update and fewer things to break at eleven on a Sunday night.

That said, if your Node app already handles the load with well-configured caching, the gain from migrating is small compared to the risk. Architecture beats runtime. An Express app behind a well-configured CDN holds up better than Bun hitting the database on every request.

The lesson from election night is simple: when everyone wants the same answer, compute it once and distribute it many times. The rest is implementation detail, and Bun keeps that detail very short.

If you want to see how I apply these ideas in real systems, take a look at the projects I've delivered.

LinkedIn summary

Midnight. A whole country hitting F5 on the same page. And the server is still up.

Election night isn't just high traffic. It's a million people asking for the same few-KB JSON at the same time.

With naive polling, 200,000 open dashboards turn into 40,000 requests per second against your own database.

The right way is to query the source once every 2 seconds and push the result to everyone. In Bun that fits in about 20 lines, with native pub/sub and zero dependencies.

Architecture beats runtime: when everyone wants the same answer, compute it once and distribute it many times.

I wrote the full step by step, with WebSocket, CDN caching and the traps that take down live dashboards. The link is in the comments.

#Bun #TypeScript #WebSocket #Performance #Backend