Skip to content

4 Ways to Get Real-Time Updates: Polling to WebSockets

Core Concept LearningJuly 4, 20265 min readUpdated July 21, 2026

Live UIs need the server to push changes — stock ticks, chat lines, build progress, presence. Classic HTTP is pull-only: the client asks, the server answers, the connection closes. Real-time patterns bend that model in four common ways: short polling, long polling, Server-Sent Events (SSE), and WebSockets.

The difference is not marketing labels — it is when requests fire, who can send next, and how much idle traffic you pay for. Place these options on a delivery timeline here; put protocol layering for microservices in Where Each Protocol Belongs.

Four patterns: short poll → long poll → SSE → WebSocket
Four patterns: short poll → long poll → SSE → WebSocket

Four patterns on a timeline

Picture client and server as two vertical lanes. Time runs downward. Short polling fills the page with tiny GET/empty pairs. Long polling draws one long wait, then a reply, then another GET. SSE opens one stream and stacks server→client events. WebSockets show a short upgrade handshake, then arrows in both directions.

Use that mental model when someone says “we need real-time.” Ask: do we need the client to push on the same channel, or only the server? How often do empty checks waste capacity? Is a corporate proxy going to block upgrades?

Time flows down: how each pattern moves bytes
Time flows down: how each pattern moves bytes

Quick reference

  • Short polling: many requests, often zero useful updates.
  • Long polling: one held request per update (or timeout).
  • SSE: one HTTP connection, many server events.
  • WebSocket: one handshake, then bidirectional frames.
  • Same product can mix patterns — e.g. REST write + SSE read.

Remember this

Each pattern trades latency against connection cost differently — the right one depends on how fresh the client actually needs to be.

Short polling

Short polling is the blunt instrument: the client asks on a timer — every 1s, 5s, 30s — whether anything changed. The server answers immediately, usually with “nothing new.” It works anywhere HTTP works and needs no special protocol support.

The cost is obvious on a busy dashboard: thousands of clients × frequent ticks = mostly empty responses, load-balancer noise, and battery drain on mobile. Prefer it only for low-frequency status (“is the job done yet?” every few seconds) or when you cannot open a held connection.

Short polling: timer → GET → reply → wait → repeat
Short polling: timer → GET → reply → wait → repeat

Quick reference

  • Best for: rare status checks, firewalled environments, tiny prototypes.
  • Strengths: trivial to implement; cacheable GETs if designed carefully.
  • Weaknesses: wasteful empty replies; lag equals your poll interval.
  • Backoff when idle; poll faster only after a user action.
  • Prefer ETag / If-None-Match to keep empty responses cheap.
  • Skip when update rate is high or latency must beat the timer.
Fixed interval — overlapping requests stampede
1setInterval(async () => {2  const response = await fetch("/api/jobs/42");3  render(await response.json());4}, 1_000);5// A slow response can overlap the next tick. During a 5xx outage,6// every client keeps retrying on the same one-second boundary.
One request at a time + jittered backoff
1async function pollJob() {2  let delayMs = 1_000;3  let etag: string | undefined;4 5  for (;;) {6    try {7      const response = await fetch("/api/jobs/42", {8        headers: etag ? { "If-None-Match": etag } : {},9        signal: AbortSignal.timeout(5_000),10      });11      if (response.status >= 500) throw new Error("server_unavailable");12      if (response.status === 200) {13        etag = response.headers.get("ETag") ?? undefined;14        render(await response.json());15      } // 304: unchanged; keep the normal interval16      delayMs = 1_000;17    } catch {18      delayMs = Math.min(delayMs * 2, 30_000);19    }20    await sleep(delayMs + Math.random() * 500);21  }22}23 24// Check: requests never overlap; a forced 503 spreads and slows retries.

Remember this

Most short-polling requests return nothing new — that empty-response waste is the price of its simplicity.

Long polling

Long polling keeps the request open until the server has something — or a timeout fires. When the response finally arrives, the client immediately opens the next request. You still speak plain HTTP, but empty chatter drops because the server does not answer until there is news.

It is the classic Comet-style fallback when WebSockets or SSE are blocked. The trade-off: each update still costs a full request/response, and held connections tie up worker capacity if your stack is thread-per-request. Set a server timeout (often 30–60s), handle 504s gracefully, and reconnect with jitter.

Long polling: server holds the GET until data or timeout
Long polling: server holds the GET until data or timeout

Quick reference

  • Best for: legacy proxies, maximum HTTP compatibility, temporary bridges.
  • Strengths: fewer empty replies than short polling; works through strict firewalls.
  • Weaknesses: still request-per-event; holding connections stresses some servers.
  • Client must re-request immediately after every response or timeout.
  • Watch connection pools and reverse-proxy idle timeouts.
  • Graduate to SSE or WebSocket when infrastructure allows.
No backoff after timeout/5xx
1async function longPoll(url: string) {2  while (true) {3    const res = await fetch(url);4    // 504/network error → tight loop can stampede the server5    if (res.ok) await handle(await res.json());6  }7}
Re-request with jitter on failure
1async function longPoll(url: string) {2  let delayMs = 500;3  for (;;) {4    try {5      const res = await fetch(url, { signal: AbortSignal.timeout(55_000) });6      if (res.status === 504 || res.status >= 500) throw new Error(`up ${res.status}`);7      if (res.ok) {8        await handle(await res.json());9        delayMs = 500; // reset after success10      }11    } catch {12      await sleep(delayMs + Math.random() * 400);13      delayMs = Math.min(delayMs * 2, 15_000);14    }15  }16}

Remember this

Long polling holds the GET open until data arrives or a timeout fires — cutting empty responses at the cost of one held connection per waiting client.

Server-Sent Events (SSE)

SSE uses one HTTP response with Content-Type: text/event-stream. After the initial GET, the server writes data: lines whenever events happen. Browsers expose this as EventSource, which reconnects automatically and can resume with Last-Event-ID.

Direction is server→client only — perfect for notifications, live feeds, progress bars, and LLM token streams. Clients still use normal REST/POST for actions. SSE rides standard HTTP (and HTTP/2 multiplexing), so load balancers and CDNs usually need less special casing than raw WebSockets.

SSE: one HTTP stream, server pushes text/event-stream
SSE: one HTTP stream, server pushes text/event-stream

Quick reference

  • Best for: feeds, notifications, progress, dashboards, one-way streams.
  • Strengths: simple HTTP; auto-reconnect; proxy-friendly; text events.
  • Weaknesses: not bidirectional; text-oriented; browser per-host connection limits.
  • Auth on the opening GET (cookie or query/header patterns your stack supports).
  • Send heartbeats/comments to keep intermediaries from closing idle streams.
  • Pair with REST writes: POST to mutate, SSE to observe.
SSE server without heartbeat
1// Express-style — idle proxies may close quiet streams2app.get("/events", (req, res) => {3  res.setHeader("Content-Type", "text/event-stream");4  res.write(`data: ${JSON.stringify({ ok: true })}\n\n`);5  // no comments/heartbeats → surprising disconnects6});
Heartbeat + client error/reconnect
1// Server2app.get("/events", (req, res) => {3  res.setHeader("Content-Type", "text/event-stream");4  res.setHeader("Cache-Control", "no-cache");5  const id = setInterval(() => res.write(": ping\n\n"), 15_000);6  req.on("close", () => clearInterval(id));7  res.write(`id: 1\ndata: ${JSON.stringify({ status: "started" })}\n\n`);8});9// Browser10const es = new EventSource("/events");11es.onmessage = (e) => handle(JSON.parse(e.data));12es.onerror = () => {13  // EventSource retries; surface UI "reconnecting…"14  console.warn("sse error; readyState=", es.readyState);15};

Remember this

SSE only needs a one-way HTTP stream, so it skips the WebSocket handshake entirely when the client never needs to push back.

WebSockets

WebSocket starts with an HTTP Upgrade handshake. On success (101 Switching Protocols), both sides exchange frames anytime — chat, multiplayer, collaborative cursors, trading UIs. Latency stays low because you skip HTTP headers on every message.

Stateful connections make horizontal scale harder: use sticky sessions or a pub/sub fabric (Redis, NATS) so any instance can fan out. Prefer battle-tested libraries (Socket.IO, ws, SignalR) with heartbeats, backoff reconnect, and message replay. Test through your real load balancer — some strip upgrade headers. Edge upgrade config belongs in NGINX Gateway.

WebSocket: HTTP upgrade, then frames either direction
WebSocket: HTTP upgrade, then frames either direction

Quick reference

  • Best for: chat, games, collab editing, live trading — any true duplex need.
  • Strengths: lowest chatty latency; binary frames; either side can speak.
  • Weaknesses: sticky/pub-sub ops; reconnect logic; some proxies fight upgrades.
  • Implement ping/pong and exponential backoff with replay.
  • Consider managed realtime (Pusher, Ably) if connection ops are not your product.
  • Do not default to WebSocket for one-way notification feeds — SSE is simpler.
Reconnect in a tight loop
1function connect() {2  const ws = new WebSocket("wss://example.com/ws");3  ws.onclose = () => connect(); // stampede on outage4}
Backoff reconnect + close codes
1function connect(attempt = 0) {2  const ws = new WebSocket("wss://example.com/ws");3  ws.onopen = () => { attempt = 0; ws.send(JSON.stringify({ type: "hello" })); };4  ws.onmessage = (e) => handle(JSON.parse(e.data));5  ws.onerror = () => ws.close();6  ws.onclose = (ev) => {7    // 1000 = normal; otherwise back off8    if (ev.code === 1000) return;9    const ms = Math.min(1000 * 2 ** attempt, 15_000) + Math.random() * 300;10    setTimeout(() => connect(attempt + 1), ms);11  };12}

Remember this

A WebSocket connection is stateful and pinned to one server process — scaling past one instance means sticky routing or a pub-sub fan-out, not just adding replicas.

Choosing the right pattern

Decision order: need client→server on the same channel? WebSocket. Server push only? SSE. Those blocked by infra? Long polling. Only rare status checks and you want zero protocol novelty? Short polling.

In microservices, produce events once (Kafka, queue, DB change feed) and let a gateway translate to SSE or WebSocket for browsers. For Next.js-style apps, stream SSE from route handlers for one-way updates; put duplex sockets on a dedicated process or managed service. Measure connection count, message rate, reconnect storms, and p99 delivery lag — not just “is it live?”

Zoom into one realtime client reconnect without losing events
Zoom into one realtime client reconnect without losing events

Quick reference

  • Bidirectional → WebSocket · server push → SSE · blocked → long poll · rare → short poll.
  • SSE + REST covers most product notifications without socket ops.
  • Kafka → realtime gateway → clients decouples producers from browsers.
  • Validate through production LB/CDN before betting on WebSocket.
  • Practice: for a “build finished” toast, implement SSE first; only add sockets if the UI must push on the same pipe.

Remember this

Directionality (one-way vs bidirectional) and infrastructure constraints — not personal preference — decide which of the four patterns fits.

Key takeaway

Real-time is a ladder of cost and capability: short poll (simple, wasteful), long poll (held HTTP), SSE (one-way stream), WebSocket (full duplex). Start from the timeline — who speaks, how often, and what your proxies allow — then pick the lightest pattern that meets the UX. Default to SSE for push feeds and WebSockets when both sides must talk; keep polling as a conscious fallback, not a habit.

Practice (20 min): Sketch client/server lanes for a live order-status page, then implement the SSE sample above (three data: events + heartbeat). Kill the server mid-stream and confirm the client shows a reconnect path. Only if the UI must push on the same pipe, swap in the WebSocket backoff sketch.

Share:

Related Articles

Selecting the correct authorization flow is essential for securing modern applications. The OAuth 2.1 specification cons

Read

Rate Limiting is a critical defense mechanism for production APIs, protecting downstream microservices from traffic spik

Read

Selecting the communication protocol between clients, API gateways, and internal microservices impacts API latency, payl

Read

Explore this topic

Keep learning

Follow a structured path or browse all courses to go deeper.