Polling, Long Polling, and Server-Sent Events

intermediate 2-4 YOE polling long-polling SSE real-time server-sent-events

Not every real-time feature needs WebSockets. Sometimes we just need the server to push updates to the client — new notifications, live scores, order status changes. Before reaching for WebSockets, we should consider simpler alternatives: polling, long polling, and Server-Sent Events. Each has its sweet spot.

Short Polling

The simplest approach. The client asks the server “any updates?” at regular intervals.

// Client polls every 5 seconds
setInterval(async () => {
  const response = await fetch('/api/notifications');
  const data = await response.json();
  if (data.length > 0) {
    showNotifications(data);
  }
}, 5000);

Think of it like checking our mailbox every 5 minutes. Most of the time, there’s nothing new. But we keep walking to the mailbox and back anyway.

Pros:

  • Dead simple to implement
  • Works everywhere (it’s just regular HTTP)
  • Stateless — server doesn’t track connections

Cons:

  • Wasteful — most requests return “nothing new”
  • Delay between events — if something happens right after we poll, we don’t know for 5 seconds
  • Scales poorly — 10,000 clients polling every 5 seconds = 2,000 requests per second for nothing

Long Polling

A smarter version of polling. The client sends a request, and the server holds it open until there’s new data. When data arrives, the server responds. The client immediately sends another request.

// Client long-polls
async function longPoll() {
  try {
    const response = await fetch('/api/notifications/subscribe');
    const data = await response.json();
    showNotifications(data);
  } catch (err) {
    // wait a bit before retrying on error
    await new Promise(r => setTimeout(r, 3000));
  }
  // immediately poll again
  longPoll();
}

longPoll();

Think of it like calling a restaurant and asking “is my table ready?” Instead of them saying “no” and hanging up (short polling), they say “hold on, I’ll let you know when it’s ready” and keep us on the line.

Pros:

  • Near real-time — data arrives as soon as the server has it
  • No wasted requests — each request results in data
  • Works through firewalls and proxies (it’s still HTTP)

Cons:

  • Server holds open connections — each waiting client ties up server resources
  • Not truly bidirectional — client can’t push data while waiting
  • Timeout handling — if the server holds too long, proxies/load balancers may kill the connection (typically 30-60 second timeout)

Server-Sent Events (SSE)

SSE is a standardized way for the server to push updates to the client over a single, long-lived HTTP connection. The client opens the connection once, and the server sends events whenever it wants.

// Client — SSE is built into browsers
const eventSource = new EventSource('/api/notifications/stream');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  showNotification(data);
};

eventSource.onerror = () => {
  console.log('Connection lost, browser will auto-reconnect');
};

Server side (Node.js):

app.get('/api/notifications/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  // Send a notification whenever we have one
  const sendEvent = (data) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  // Subscribe to notification events
  notificationEmitter.on('notify', sendEvent);

  req.on('close', () => {
    notificationEmitter.off('notify', sendEvent);
  });
});

The server sends data in a specific format:

data: {"type": "notification", "message": "New follower!"}

data: {"type": "notification", "message": "Your order shipped!"}

event: heartbeat
data: ping

Pros:

  • Built into browsers — EventSource API with automatic reconnection
  • Simple — just HTTP with a special content type
  • Efficient — single connection, server pushes when ready
  • Auto-reconnect — the browser handles reconnection automatically

Cons:

  • One-directional — server to client only. Client can’t send data over the same connection.
  • Connection limits — browsers limit SSE connections per domain (6 in HTTP/1.1, more in HTTP/2)
  • Text only — no binary data (but we can base64 encode if needed)

Comparison: All Four Approaches

Timeline: How Each Approach Works
Short Polling
Client: req→ ←(empty)   req→ ←(empty)   req→ ←(DATA!)   req→ ←(empty)
Most requests return nothing. Simple but wasteful.
Long Polling
Client: req→ ........←(DATA!)   req→ ..←(DATA!)   req→ .......
Server holds until data arrives. Fewer wasted requests.
Server-Sent Events
Client: req→ ═══ ←(DATA!) ═══ ←(DATA!) ═══ ←(DATA!) ═══
One connection. Server pushes whenever. Client can't send.
WebSocket
Client: upgrade→ ═ →msg ←msg ←msg →msg ←msg →msg ═
One connection. Both sides send anytime. Full bidirectional.
FeatureShort PollingLong PollingSSEWebSocket
DirectionClient → ServerClient → ServerServer → ClientBoth ways
LatencyUp to poll intervalNear real-timeReal-timeReal-time
ComplexityVery lowLowLowMedium-High
Server resourcesLow per requestMedium (held connections)Medium (held connections)Medium (held connections)
Browser supportEverywhereEverywhereAll modern browsersAll modern browsers
Auto-reconnectN/AManualBuilt-inManual
Binary dataYesYesNo (text only)Yes
ScalabilityPoor (wasted requests)OKGoodGood
Firewall friendlyYesYesYesSometimes no

When to Use Each

Short Polling — When updates are infrequent and slight delay is fine. Weather data, email checking. Dead simple to implement.

Long Polling — When we need near real-time but can’t use WebSocket or SSE. Good fallback. Used by older chat systems.

SSE — When we only need server-to-client push. Live notifications, news feeds, stock tickers, progress bars, live logs. This is the underrated option — it’s simpler than WebSocket and handles 90% of “real-time” use cases.

WebSocket — When we need full bidirectional communication. Chat, multiplayer games, collaborative editing. Worth the extra complexity only when both sides need to send data frequently.

A Real-World Decision Tree

Do we need real-time updates?
  ├─ No → Regular HTTP (REST)
  └─ Yes → Does the client need to send data too?
      ├─ No → SSE (simplest real-time option)
      └─ Yes → How often does the client send?
          ├─ Rarely → SSE + regular HTTP POST for client messages
          └─ Frequently → WebSocket

The often-overlooked option is SSE + REST. We use SSE for the server pushing updates and regular POST requests when the client needs to send something. This gives us 80% of WebSocket functionality with way less complexity.

Key Takeaway

In simple language, polling is the client repeatedly asking “anything new?” — simple but wasteful. Long polling improves this by having the server wait until there IS something new. SSE opens a one-way channel where the server pushes updates as they happen. WebSocket opens a two-way channel for full real-time communication. Start with the simplest option that meets our needs — often that’s SSE, not WebSocket.