Complete HTML Course Module 14: Advanced: HTML Web APIs

HTML Server-Sent Events (SSE): Live Updates from the Server

  • Beginner
  • Lesson 14.6
  • 6 min read
  • 63 of 69 in the course

In short

HTML Server-Sent Events (SSE) let the server push live updates to a web page over one open connection, using the EventSource API. They are perfect for live scores, news feeds and notifications. Normally the browser asks and the server answers, then the connection ends.

HTML Server-Sent Events (SSE) let the server push live updates to a web page over one open connection, using the EventSource API. They are perfect for live scores, news feeds and notifications.

Normally the browser asks and the server answers, then the connection ends. With SSE, the browser asks once and the server keeps the line open, sending new messages whenever it has them. Think of it like a radio: you tune in once and the station keeps talking.

How Server-Sent Events work

  1. The page creates an EventSource pointing to a URL on your server.
  2. The server answers with the content type text/event-stream and does not close the connection.
  3. Whenever it wants, the server writes a message in a simple text format.
  4. The browser fires an event in your page for each message.
  5. If the connection drops, the browser reconnects automatically.

SSE is one-way: server to browser. To send something back, the page uses a normal form or fetch() request.

The event stream format

The messages are plain text. Each message is one or more field: value lines, followed by a blank line that ends the message:

data: Goal! Lions 1 - 0 Tigers

event: score
data: {"home": 1, "away": 0}

id: 42
retry: 5000
data: Half time
FieldMeaning
data:The message text. Several data: lines are joined with a new line
event:A name for the event. Without it, the event is called message
id:A message number. After a reconnect, the browser sends the last id in a Last-Event-ID header so the server can continue
retry:How many milliseconds to wait before reconnecting

Lines that start with : are comments, often sent to keep the connection alive.

A tiny PHP server example

Save this as clock.php in your XAMPP htdocs folder. It sends the server time every second:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');

set_time_limit(0);
while (ob_get_level() > 0) {
    ob_end_flush();
}

for ($i = 1; $i <= 30; $i++) {
    if (connection_aborted()) {
        break;
    }
    $time = date('H:i:s');
    echo "id: {$i}\n";
    echo "data: Server time is {$time}\n\n";
    flush();
    sleep(1);
}
  • The first header() line is the key: it tells the browser this is an event stream.
  • Cache-Control: no-cache stops the browser or proxies from saving old messages. X-Accel-Buffering: no stops some servers (like Nginx) from holding messages back.
  • set_time_limit(0) lets the script run longer than PHP's usual 30-second limit.
  • The while loop turns off PHP's output buffering, so messages are sent at once instead of being collected.
  • The for loop sends 30 messages. Each one has an id, a data line and the blank line (\n\n) that ends it.
  • flush() pushes the message to the browser right now. sleep(1) waits one second.
  • connection_aborted() checks if the user closed the page, so the script can stop.

After 30 messages the script ends. That is fine: the browser waits a few seconds and reconnects, and a new run starts. Short-running scripts are actually kinder to a PHP server than endless ones.

Receiving events with EventSource and onmessage

Now the page, clock.html, in the same folder:

<h1>Live server clock</h1>
<p id="clock" aria-live="polite">Connecting…</p>
<button type="button" id="stop">Stop updates</button>

<script>
  const clock = document.querySelector("#clock");
  const source = new EventSource("clock.php");

  source.onmessage = (event) => {
    clock.textContent = event.data;
  };

  source.onerror = () => {
    clock.textContent = "Connection lost. Reconnecting…";
  };

  document.querySelector("#stop").addEventListener("click", () => {
    source.close();
    clock.textContent = "Updates stopped.";
  });
</script>
  • new EventSource("clock.php") opens the connection.
  • source.onmessage runs for every message without an event: name. event.data holds the text after data:.
  • source.onerror runs when the connection fails or the server script ends. The browser will try again by itself.
  • source.close() closes the connection for good; the browser will not reconnect.
  • aria-live="polite" lets screen readers announce updates without interrupting.

Open http://localhost/clock.html. The time updates every second, straight from the server.

Common mistake: Messages arrive all at once after 30 seconds instead of one by one. That means something is buffering the output. Check that you call flush() after each message and turned off output buffering. Compression (gzip) on the server can also buffer, so disable it for the stream.

Named events: different kinds of messages

With the event: field, one stream can carry different kinds of messages. You listen for each name with addEventListener(). Here is a live football score. The server sends:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
while (ob_get_level() > 0) { ob_end_flush(); }

$score = ['home' => 2, 'away' => 1];
echo "event: score\n";
echo 'data: ' . json_encode($score) . "\n\n";

echo "event: news\n";
echo "data: Yellow card for number 7\n\n";
flush();

And the page listens:

<p>Lions <strong id="home">0</strong> - <strong id="away">0</strong> Tigers</p>
<ul id="news"></ul>

<script>
  const source = new EventSource("match.php");

  source.addEventListener("score", (event) => {
    const score = JSON.parse(event.data);
    document.querySelector("#home").textContent = score.home;
    document.querySelector("#away").textContent = score.away;
  });

  source.addEventListener("news", (event) => {
    const item = document.createElement("li");
    item.textContent = event.data;
    document.querySelector("#news").prepend(item);
  });
</script>
  • The score message carries JSON, so JSON.parse() turns it back into an object with home and away.
  • The news message is plain text, added to the top of the list with prepend.
  • Named events do not trigger onmessage; they only reach their own listener.

You will see "Lions 2 - 1 Tigers" and the news line. Because this short script ends at once, the browser reconnects after about 3 seconds and receives the same messages again, so the news line repeats. In a real app, the PHP script would loop, read new data from a database, and send only what has changed.

SSE vs WebSockets vs polling

There are three common ways to get live data. Which should you use?

PollingServer-Sent EventsWebSockets
How it worksThe page asks again every few seconds with fetch()One open connection, server pushesOne open connection, both sides push
DirectionBrowser asks, server answersServer to browser onlyBoth ways
Data typeAnythingText (often JSON)Text or binary
Auto reconnectNot neededYes, built inNo, you write it
Server needsAny serverAny HTTP server (PHP works)A WebSocket server
DelayUp to the polling intervalAlmost instantAlmost instant
Best forRare updates, simple setupsFeeds, scores, notifications, dashboards, AI text streamingChat, multiplayer games, collaboration

Note: Over old HTTP/1.1 connections, a browser allows only about 6 open connections per site, and each SSE stream uses one. With HTTP/2 or HTTP/3 (used by most HTTPS sites today) this limit is no longer a real problem.

Practice exercise

Make a "Server stats" page. Write stats.php that sends a stats named event every 2 seconds with JSON like {"visitors": 37, "time": "14:05:10"} (use rand(10, 100) for fake visitors), for 20 rounds. On the page, show both values and add a "Pause" button that closes the connection.

Hint: Build the data with json_encode(['visitors' => rand(10, 100), 'time' => date('H:i:s')]), and remember event: stats, the blank line, flush() and sleep(2).

Key takeaways

  • Server-Sent Events push messages from the server to the page over one open HTTP connection.
  • The server sends Content-Type: text/event-stream and messages made of data:, event:, id: and retry: lines, each ending with a blank line.
  • In the page, new EventSource(url) connects, onmessage handles unnamed messages and addEventListener("name", ...) handles named ones.
  • The browser reconnects automatically; source.close() stops it.
  • Choose SSE for one-way live updates, WebSockets for two-way, and polling for rare, simple updates.