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
- The page creates an
EventSourcepointing to a URL on your server. - The server answers with the content type
text/event-streamand does not close the connection. - Whenever it wants, the server writes a message in a simple text format.
- The browser fires an event in your page for each message.
- 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
| Field | Meaning |
|---|---|
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-cachestops the browser or proxies from saving old messages.X-Accel-Buffering: nostops 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
whileloop turns off PHP's output buffering, so messages are sent at once instead of being collected. - The
forloop sends 30 messages. Each one has anid, adataline 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.onmessageruns for every message without anevent:name.event.dataholds the text afterdata:.source.onerrorruns 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
scoremessage carries JSON, soJSON.parse()turns it back into an object withhomeandaway. - The
newsmessage is plain text, added to the top of the list withprepend. - 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?
| Polling | Server-Sent Events | WebSockets | |
|---|---|---|---|
| How it works | The page asks again every few seconds with fetch() | One open connection, server pushes | One open connection, both sides push |
| Direction | Browser asks, server answers | Server to browser only | Both ways |
| Data type | Anything | Text (often JSON) | Text or binary |
| Auto reconnect | Not needed | Yes, built in | No, you write it |
| Server needs | Any server | Any HTTP server (PHP works) | A WebSocket server |
| Delay | Up to the polling interval | Almost instant | Almost instant |
| Best for | Rare updates, simple setups | Feeds, scores, notifications, dashboards, AI text streaming | Chat, 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-streamand messages made ofdata:,event:,id:andretry:lines, each ending with a blank line. - In the page,
new EventSource(url)connects,onmessagehandles unnamed messages andaddEventListener("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.