← Writing

Before Server-Sent Events: The Cost of Asking for Updates

October 8, 20266 min read

javascriptsseeventsourcerealtime

Abstract

Real-time web interfaces need timely server state without requiring the client to invent a transport. Before Server-Sent Events (SSE) were standardized in browsers, applications typically approximated push with polling or long polling (hanging GET / COMET). Both approaches inherit the request–response shape of HTTP and therefore impose overhead, latency trade-offs, or fragile client hacks. This note reconstructs that problem from the account in web.dev’s SSE primer, then situates SSE and the EventSource API as a purpose-built, unidirectional remedy. A live demonstration accompanies the discussion.

1. Introduction

HTTP was designed around a client-initiated exchange: the browser asks; the server answers; the conversation ends. Many product surfaces, however, are asymmetric. The interesting work happens on the server (a job finishes, a feed item appears, a price moves), and the client’s job is only to observe.

That mismatch produced a long period in which developers simulated “the server talking first” using AJAX patterns that were never designed for continuous notification. As Bidelman notes, understanding SSE begins with understanding the limits of those predecessors—chiefly polling and long polling (web.dev).

Research question. What structural problems did pre-SSE techniques create when an application needed automatic updates from server to client, and how does SSE address them without inventing a second protocol?

2. Problem statement: updates before SSE

2.1 Polling

In classical polling, the client repeatedly issues requests asking whether new data exists. If nothing is ready, the server returns an empty or “no change” response. The client waits a short interval and asks again.

This technique dominated early AJAX applications because it required no special server semantics: any REST endpoint could be polled. The cost is structural:

  1. HTTP overhead. Each poll is a full request–response cycle—headers, connection work, application handlers—even when the payload is empty.
  2. Latency vs. load trade-off. Short intervals reduce perceived delay but multiply idle traffic. Long intervals save bandwidth but make the UI feel late.
  3. Client-driven timing. The server cannot speak when an event occurs; it can only answer the next question. Freshness is bounded by the poll period, not by the event time.

In short, polling answers “is there news?” on a schedule chosen by the client, not “here is news” at the moment the server knows.

2.2 Long polling (hanging GET / COMET)

Long polling improves on empty replies. If the server has no data, it holds the request open until something arrives (or a timeout fires). When data is available, the server responds, closes the connection, and the client immediately opens a new hanging request. The pattern is often called a hanging GET or associated with COMET-style techniques (web.dev).

Long polling reduces wasted empty responses, but it does not escape the request–response unit:

  1. One event, one closed response. Delivery still ends the HTTP exchange. Continuity is reconstructed by the client opening the next request.
  2. Reconnect churn. Under a busy event stream, the application pays connection setup costs repeatedly.
  3. Implementation fragility. Historically, developers resorted to hacks—such as appending script tags to an “infinite” iframe—to keep a channel open (web.dev). Those techniques were brittle across browsers and proxies.

Thus long polling is a better approximation of push, not push itself: the server delays the answer, but the protocol still thinks in closed responses.

2.3 Synthesis of the pre-SSE problem

TechniqueWho initiates?When does the connection end?Primary failure mode
PollingClient, on a timerAfter every answer (including empty)Overhead and stale UI
Long pollingClient, after each deliveryAfter each non-empty answerReconnect churn; hacks
Ideal pushServer, when events existOnly when the subscription ends(not available in stock AJAX)

The shared constraint is the absence of a first-class, browser-managed subscription to a server-authored event stream. Until that existed, real-time UIs paid either in traffic, in latency, or in engineering complexity.

3. Method: Server-Sent Events as a designed response

SSE was specified so that, once a connection is established, the server may initiate transmission over that HTTP response. The channel is unidirectional: server → client. The browser exposes the subscription through EventSource; the application listens rather than inventing a poll loop (web.dev; MDN).

Unlike long polling, the response is not closed after each message. Unlike WebSockets, SSE does not require a separate full-duplex protocol: events travel as text/event-stream over ordinary HTTP, with browser-native reconnection, event IDs, and named event types (web.dev).

4. Empirical demonstration

The widget below is an instrumented instance of the pattern under study. Selecting Connect constructs new EventSource("/api/sse"). The route handler keeps one response open, emits a tick each second, emits a named ping every five ticks, and terminates after fifteen ticks (with a 20s hard cap). The browser client also auto-closes after 20s and on tab hide / navigation, so a forgotten demo cannot hold the stream open.

Connection: idle

Live stream from /api/sse · ends in ~15 server ticks or 20s client max · closes when you leave

  • Click Connect. The server keeps one HTTP response open and pushes ticks. It will shut itself down.

Observation protocol: open developer tools, inspect /api/sse as an EventStream, and note that multiple data: frames arrive on a single HTTP response—behavior polling and long polling cannot express without closing and reopening.

4.1 Client subscription

const source = new EventSource("/api/sse");

source.addEventListener("message", (e) => {
  console.log(JSON.parse(e.data));
});

source.addEventListener("ping", (e) => {
  console.log("named event", JSON.parse(e.data));
});

source.close(); // cancel; disables auto-reconnect

4.2 Wire format (minimal)

Content-Type: text/event-stream

data: {"tick":1,"message":"Hello from the server (#1)"}\n\n
event: ping\n
data: {"tick":5,"note":"every 5 ticks"}\n\n

A blank line terminates each event. Optional id: and retry: fields support resume and reconnect policy (web.dev).

5. Discussion: scope and alternatives

SSE addresses the one-way notification problem cleanly. It does not replace WebSockets where the application needs a continuous, bidirectional channel (games state, collaborative editing, chat on one socket). In the unidirectional case—status feeds, progress, server-authored alerts—SSE avoids both the empty-request tax of polling and the per-event teardown of long polling, while keeping the transport inside HTTP.

Limitations remain: EventSource is GET-only; cross-origin use requires CORS; intermediaries may buffer streams; and without HTTP/2, browsers historically limited concurrent connections per origin (MDN).

6. Conclusion

The problem before SSE was not a lack of creativity—it was a missing abstraction. Polling made the client guess when to ask. Long polling made the server withhold the answer, then forced another handshake. SSE makes the subscription itself a browser primitive: one open response, server-initiated frames, automatic recovery. The pre-SSE techniques remain useful historically and occasionally tactically; they are no longer the right default for unidirectional live updates on the web.

References

  1. E. Bidelman, “Stream updates with server-sent events,” web.dev. https://web.dev/articles/eventsource-basics
  2. MDN Web Docs, “EventSource.” https://developer.mozilla.org/en-US/docs/Web/API/EventSource
  3. MDN Web Docs, “Using server-sent events.” https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events