Architecture

Real-Time Architectures: Polling vs SSE vs WebSocket vs Push — How to Choose

Learn when to use Polling, SSE, WebSocket or Push Notifications in a real-time architecture, based on who sends data, latency and whether the app is open.

Aarav Sharma

Aarav Sharma

October 26, 202610 min read
Share
Choosing a real-time architecture by asking who sends data and whether the app is open, with the simplest fit winning over WebSocket by default

Your app needs to show information the moment it changes, and someone on the team asks the usual question: "Should we use WebSocket?" It sounds like the obvious answer, because WebSocket is the technology most people associate with real-time.

But WebSocket is only one of four ways to get fresh data to a user. You can also use Polling, Server-Sent Events (SSE) or Push Notifications, and each one solves a different communication problem. Pick the wrong one and you either ship a feature that feels slow or run infrastructure you never needed.

The real question is not which technology is most advanced. It is how your feature actually needs to communicate. This post walks through all four, shows when each one fits, and gives you a short list of questions that lands you on the right one.

Quick Answer

Use Polling when checking for updates every few seconds is good enough. Use SSE when the server mainly needs to stream updates to a client that has the page open. Use WebSocket when the client and server both need to send messages continuously. Use Push Notifications when the user has to hear about an event even when the app is closed. Choose per feature, and pick the simplest option that meets the requirement.

What Is Real-Time Architecture?

Plain HTTP follows a request-response model. The client asks, the server answers, and the exchange is over. That works for most APIs, but not when data changes after the first response:

  • An order moves from Preparing to Shipped.
  • A payment moves from Pending to Successful.
  • A new chat message arrives.
  • A background job finishes.

Real-time architecture is how the client finds out about those changes. It does not mean zero latency. One feature can live with a five-second delay, while another needs updates within milliseconds.

So you choose based on a handful of questions: how fast the update has to arrive, who sends the data, whether a connection has to stay open, how often the data changes, whether the app can be closed, and how much operational work your team can take on.

The Problem

Say a user opens your order-tracking page. The server returns Preparing. Five seconds later the order becomes Shipped. Your server knows the state changed. The browser does not.

There are four ways to close that gap:

  • The client asks again. That is Polling.
  • The server streams updates down an open response. That is SSE.
  • Both sides keep a connection open and talk. That is WebSocket.
  • The system notifies the device through a third party. That is Push.

To the user, all four can look the same: the status on screen changes. Under the hood they behave very differently, and the difference shows up in your server load, your infrastructure and your on-call rota.

The Solution: Match the Channel to the Communication Pattern

Here is a small Node.js server that shows the first two approaches side by side on the same order. It has no dependencies. Save it as server.js, run node server.js and open http://localhost:3000.

// server.js: run with `node server.js`, then open http://localhost:3000
const http = require('node:http')

let status = 'Preparing'
const streams = new Set()

function setStatus(next) {
  status = next
  for (const res of streams) res.write(`data: ${JSON.stringify({ status })}\n\n`)
}

// Simulate the order moving on
setTimeout(() => setStatus('Shipped'), 5_000)
setTimeout(() => setStatus('Delivered'), 10_000)

const page = `<!doctype html>
<p>Polling: <span id="poll">...</span></p>
<p>SSE: <span id="sse">...</span></p>
<script>
  // Polling: ask the server every 3 seconds
  setInterval(async () => {
    const res = await fetch('/orders/42')
    const order = await res.json()
    document.getElementById('poll').textContent = order.status
  }, 3000)

  // SSE: open one stream and let the server tell you
  const events = new EventSource('/orders/42/events')
  events.onmessage = (event) => {
    document.getElementById('sse').textContent = JSON.parse(event.data).status
  }
</script>`

http.createServer((req, res) => {
  if (req.url === '/orders/42') {
    res.writeHead(200, { 'Content-Type': 'application/json' })
    return res.end(JSON.stringify({ status }))
  }

  if (req.url === '/orders/42/events') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    })
    res.write(`data: ${JSON.stringify({ status })}\n\n`)
    streams.add(res)
    req.on('close', () => streams.delete(res))
    return
  }

  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end(page)
}).listen(3000, () => console.log('Open http://localhost:3000'))

Watch the page. The SSE line flips to Shipped the instant the server changes it. The Polling line only catches up on its next request, up to 3 seconds later. That gap is the whole trade-off between the first two approaches.

1. Polling: the client keeps asking

With Polling, the client repeats a normal GET on a timer and asks for the latest state. In the example, the browser sends GET /orders/42 at 0 s, 3 s and 6 s. The status changes at 5 s, so the first two requests return Preparing and only the 6 s request returns Shipped.

The client sends GET /orders/42 at 0 s, 3 s and 6 s; the first two responses say Preparing and only the 6 s response says Shipped, after the status changed at 5 s

A shorter interval catches changes sooner but sends more requests, most of which return nothing new. RFC 6202 describes this trade-off between latency and resource use.

Polling is simple to build, works with any REST API, needs no persistent connection, and is easy to debug and monitor. Use it when a small delay is fine and data changes rarely. A dashboard that refreshes every 30 seconds does not need WebSocket. Avoid aggressive polling when updates must appear almost at once or when thousands of clients would mostly get unchanged answers.

Mental model: Polling means "I keep asking."

2. Server-Sent Events: one request, many events

With SSE, the client makes one request to GET /orders/42/events and the server never finishes the response. It keeps a single text/event-stream response open and writes a data: line every time something changes: data: Preparing, then data: Shipped, then data: Delivered.

The client sends one GET /orders/42/events request and the server writes data: Preparing, data: Shipped and data: Delivered down the same open text/event-stream response

Browsers receive these events through the EventSource API, which also reconnects for you if the connection drops. The format is defined in the HTML Standard.

The stream is one-way, server to client. Your client can still send normal HTTP requests, though. For example, it can start a report with POST /reports and then listen on SSE as the job reports progress until the report is ready.

SSE fits live dashboards, activity feeds, progress bars and monitoring screens, anywhere the client mainly listens. It is the wrong choice when the client needs to send a steady flow of messages back, as in a multiplayer game.

Mental model: SSE means "Tell me when something changes."

3. WebSocket: both sides send at any time

WebSocket starts as an HTTP request carrying an Upgrade: websocket header. The server answers 101 Switching Protocols, and from then on the same connection carries messages in both directions. RFC 6455 defines the protocol.

After an Upgrade: websocket request and a 101 Switching Protocols response, the client sends Hello, the server replies Message delivered, and the server forwards Hi from another client

Take a chat app. Your client sends Hello. The server replies Message delivered. A moment later another user sends Hi, and the server forwards it to you over the same connection. The server holds the open socket, and neither side waits for the other to ask.

That makes WebSocket the right tool for chat, multiplayer games, collaborative editing and trading screens. It also hands you more to own: connection lifecycle, reconnection, authentication on a long-lived socket, load balancing, message ordering, duplicates and recovery. The browser's WebSocket interface does not support backpressure, so if messages arrive faster than your code handles them, they pile up in memory or pin the CPU.

Do not reach for WebSocket only because a feature needs "real-time data." If only the server sends, SSE is simpler. If checking every 30 seconds is enough, Polling is simpler still.

Mental model: WebSocket means "Let's keep talking both ways."

4. Push Notifications: delivery through a push service

Push solves a different problem. You are not keeping a live connection with an open app. You are trying to reach the user when they are not looking at it.

Your app server sends a push request to a push service run by the browser or OS vendor. The push service delivers it to the device, where a service worker wakes up and shows the notification. If the device is offline, the push service queues the message until its TTL expires, as described in RFC 8030.

The app server sends a push request to the push service, which queues it until the TTL expires and delivers it to the device, where a service worker wakes

The Push API lets a web app receive these messages whether or not it is in the foreground, or even loaded, as long as the user has opted in. That makes Push the right call for order and payment updates, security alerts, reminders and new-message notifications. A user closes your app, the order ships later, and they still see "Your order has been shipped."

Do not use Push for live dashboards, high-frequency streams or interactive features. It is a notification channel, not a data pipe.

Mental model: Push means "Notify me even when I'm not connected."

5. Pick the simplest channel that fits

Start with the requirement, not the technology. Ask these questions in order and stop at the first yes:

  1. Is a short delay fine? Use Polling.
  2. Does only the server send? Use SSE.
  3. Do both sides send often? Use WebSocket.
  4. Can the app be closed when the event happens? Use Push.
A requirement branches into four questions: is a short delay fine leads to Polling, does only the server send leads to SSE, do both sides send often leads to WebSocket, and can the app be closed leads to Push

Make the call one feature at a time, not once per app. One product can use normal HTTP for user actions, SSE for a live dashboard, WebSocket for chat and Push for background alerts. That usually beats forcing one technology to cover every case. The simplest fit wins, and it is not WebSocket by default.

RequirementPollingSSEWebSocketPush
Who sendsClient asksServer to clientBoth directionsServer to device
ConnectionRepeated requestsOne long-lived responseOne long-lived socketThrough a push service
Two-wayNoNoYesNo
Works when app is closedNoNoNoYes
ComplexityLowLow to mediumMedium to highMedium
Typical usePeriodic dashboard refreshLive progress and statusChat and gamesAlerts and reminders

Trade-offs

Every option here costs you something, and the right one for one feature is the wrong one for the next.

  • Polling wastes work. Most requests return the same answer, and your latency can never beat the interval. Polling every second for thousands of users is a very different system from polling every 30 seconds.
  • SSE ties up connections. Each open tab holds a response open on your server, so your load balancer and proxies need long timeouts. Over HTTP/1.1, browsers allow only 6 SSE connections per browser and domain across all tabs. HTTP/2 lifts that limit.
  • WebSocket moves the hard parts to you. Sticky sessions or a shared message bus, reconnect logic, auth on a long-lived socket, ordering and missing backpressure all become your problem. Choose it when you truly need two-way traffic, not as a default.
  • Push is not guaranteed. Users can deny permission, subscriptions expire, and a message that outlives its TTL is dropped. Treat it as a nudge and keep the source of truth in your app.

Real-time is the wrong goal when nobody needs the data instantly. If users check a report once a day, a page refresh is the right architecture. Each of these channels adds moving parts you have to monitor, so only add one when a real requirement calls for it.

Conclusion

On Monday, list every feature in your app that shows changing data and ask three questions: who sends data (the server only, or both sides), how fast must it arrive, and is the app open (an active tab, or closed)? Start each feature on Polling and move to SSE, WebSocket or Push only when one of those answers demands it. Before you commit to anything heavier, run the server.js example above and watch the gap between the Polling and SSE lines.

Contact us

Aarav Sharma

Aarav Sharma

Lead Software Engineer

Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.

Related articles

Let's Collaborate

Tell us about your project and we'll come back with a plan, a timeline, and a quote.

Project Type

Budget

Task Message

Your Contacts