KAHIBARO
Discord Login Register

27.2. Server-Sent Events

Understanding Server-Sent Events

Server-Sent Events (SSE) let a server push data to the browser over a single long‑lived HTTP connection. Unlike WebSockets, SSE is one‑way, from server to client, and is built directly on top of HTTP.

SSE is a good fit when the client only needs to receive updates, such as notifications, live logs, progress updates, or live scores.

Basic Idea of Server-Sent Events

With normal HTTP, the client makes a request, the server responds, and the connection closes. With SSE, the client makes one request and the server keeps that HTTP connection open. Whenever there is new data, the server sends a small text message over the same connection.

In the browser, you use the EventSource API to connect to an SSE endpoint:

js
const source = new EventSource('/events');
source.onmessage = (event) => {
  console.log('New message:', event.data);
};
source.onerror = (err) => {
  console.error('SSE error', err);
};

On the backend, you expose an endpoint that:

  1. Uses HTTP GET.
  2. Returns content type text/event-stream.
  3. Never fully finishes, or finishes only when the stream ends.
  4. Sends data in a specific text format.

Key properties of SSE:

  • Client connects using HTTP GET to an endpoint.
  • Response header must include Content-Type: text/event-stream.
  • Messages are sent as UTF‑8 encoded text lines that follow the SSE format.
  • The HTTP connection is kept open as long as possible.
  • The browser will automatically reconnect if the connection drops, unless disabled.

The SSE Message Format

SSE messages are just lines of text with special prefixes. The most common are:

An SSE stream is a sequence of messages separated by a blank line.

Minimal message

The simplest message has only a data: line:

text
data: Hello, world!

The blank line at the end means "end of this message".

Multiline data

If the message body spans multiple lines, repeat data::

text
data: Line 1
data: Line 2
data: Line 3

The browser will join them with \n into one event.data string.

Custom event names

You can name events and listen to them separately:

text
event: notification
data: You have a new message
event: metrics
data: {"cpu": 0.42, "memory": 0.78}

On the client:

js
const source = new EventSource('/events');
source.addEventListener('notification', (event) => {
  console.log('Notification:', event.data);
});
source.addEventListener('metrics', (event) => {
  const metrics = JSON.parse(event.data);
  console.log('Metrics:', metrics);
});

If you omit event:, the browser treats it as a message event and calls onmessage.

Message IDs and retry

The id: line assigns an ID to a message, which browsers use for automatic reconnection. The retry: line tells the client how long to wait before reconnecting.

Example:

text
id: 1
retry: 5000
data: First message
id: 2
data: Second message

If the connection drops, the browser will:

You can use Last-Event-ID on the server to resume from where you stopped.

SSE vs WebSockets vs Polling

SSE is one tool among several for server to client updates.

FeatureSSEWebSocketsLong / Short Polling
DirectionServer → Client onlyBidirectionalClient → Server (pull)
ProtocolHTTPCustom over TCP / HTTP upgradeHTTP
Browser APIEventSourceWebSocketfetch() / XMLHttpRequest
Message formatText (UTF‑8)Text or binaryAny HTTP response
Auto reconnectionYes, built‑inManualNot needed, new request each time
Works with proxiesUsually yes, plain HTTPSometimes trickyYes
ComplexityLowHigherLow to medium

Use SSE when:

If you need the client to send frequent real‑time messages to the server, WebSockets are usually a better fit.

HTTP Response Requirements

For an SSE endpoint, the HTTP response must be carefully set up.

Required headers

Basic headers:

http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

Details:

In some setups, you may also add:

Streaming and flushing

The server must:

In many frameworks, this means using streaming responses or generator functions.

Example: Simple SSE Endpoint (Python / FastAPI)

FastAPI does not have a special SSE type, but you can use StreamingResponse to send the event stream.

Backend example:

python
import asyncio
import json
from fastapi import FastAPI
from starlette.responses import StreamingResponse
app = FastAPI()
async def event_generator():
    counter = 0
    while True:
        await asyncio.sleep(1)
        counter += 1
        data = {"counter": counter}
        # Build SSE message
        message = f"data: {json.dumps(data)}\n\n"
        yield message
@app.get("/events")
async def events():
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
    )

Client example:

html
<!doctype html>
<html>
  <body>
    <div id="output"></div>
    <script>
      const output = document.getElementById('output');
      const source = new EventSource('/events');
      source.onmessage = (event) => {
        const data = JSON.parse(event.data);
        output.textContent = 'Counter: ' + data.counter;
      };
      source.onerror = (err) => {
        console.error('SSE error', err);
      };
    </script>
  </body>
</html>

Every second, the client receives a new message and updates the page.

Custom Event Types Example

You can use event: to define different types of updates. For instance, suppose you have three kinds of events:

Backend (simplified):

python
async def event_generator():
    # status event
    yield "event: status\ndata: started\n\n"
    await asyncio.sleep(1)
    # log event
    yield "event: log\ndata: Step 1 completed\n\n"
    await asyncio.sleep(1)
    # progress event
    yield "event: progress\ndata: 50\n\n"
    await asyncio.sleep(1)
    # final status
    yield "event: status\ndata: finished\n\n"

Client:

js
const source = new EventSource('/job-events');
source.addEventListener('status', (event) => {
  console.log('Status:', event.data);
});
source.addEventListener('log', (event) => {
  console.log('Log:', event.data);
});
source.addEventListener('progress', (event) => {
  console.log('Progress:', event.data + '%');
});

Handling Reconnection and Last-Event-ID

Browsers automatically reconnect on errors. You can use message IDs to continue from the last event.

Backend:

python
from fastapi import Request
async def event_generator(last_id: int):
    current = last_id
    while True:
        await asyncio.sleep(1)
        current += 1
        data = {"counter": current}
        message = f"id: {current}\ndata: {json.dumps(data)}\n\n"
        yield message
@app.get("/events")
async def events(request: Request):
    # Read Last-Event-ID, default to 0 if missing
    last_id_header = request.headers.get("Last-Event-ID")
    last_id = int(last_id_header) if last_id_header is not None else 0
    generator = event_generator(last_id)
    return StreamingResponse(generator, media_type="text/event-stream")

If the connection drops after message with id: 5, on reconnection the browser will send:

http
Last-Event-ID: 5

Your server can use this to resume from ID 6.

Important SSE reconnection rules:

  • Each message can have an id: line.
  • The browser remembers the last id it saw.
  • On reconnect, the browser sends Last-Event-ID header.
  • You can use Last-Event-ID to avoid resending old events.

Common Use Cases

SSE is especially useful for:

Live notifications

For example, notifying a user when:

The client opens an EventSource when the web app loads and listens for notification events.

Live dashboards

For metrics and logs that need continuous updates:

The server broadcasts new metrics to all connected SSE clients.

Progress updates for long tasks

Instead of polling a /status endpoint, the browser can:

This is often simpler than maintaining WebSocket connections.

Practical Considerations and Limitations

Even though SSE is simple, you need to be aware of some details.

Connection limits

Browsers limit how many concurrent connections can be open to the same domain. This includes SSE connections. Often the limit is around 6 connections.

Avoid opening too many SSE streams per page. Usually, you want only one EventSource and then multiplex different event types over it.

Timeouts and proxies

Some proxies or load balancers close idle connections. Because SSE keeps connections open and can be relatively idle, you may need:

text
  : heartbeat

Lines beginning with : are comments and are ignored by the browser, but keep the connection active.

No binary data

SSE is text only. If you need to send binary data, you must encode it, for example with Base64, or use WebSockets instead. In practice, most SSE messages are JSON text, which works well.

CORS and authentication

SSE uses HTTP, so all normal HTTP concerns apply:

Many applications secure SSE endpoints with the same mechanisms as their normal APIs.

Example: Incremental Logs via SSE

Imagine you have a background process that produces logs as it runs. You can stream those logs to the client in real time.

Backend example structure:

python
import asyncio
from fastapi import FastAPI
from starlette.responses import StreamingResponse
app = FastAPI()
async def log_stream():
    logs = [
        "Starting job",
        "Downloading data",
        "Processing data",
        "Saving results",
        "Job complete"
    ]
    for line in logs:
        await asyncio.sleep(1)
        yield f"event: log\ndata: {line}\n\n"
@app.get("/job/logs")
async def job_logs():
    return StreamingResponse(log_stream(), media_type="text/event-stream")

Client:

js
const source = new EventSource('/job/logs');
source.addEventListener('log', (event) => {
  const el = document.createElement('div');
  el.textContent = event.data;
  document.body.appendChild(el);
});

The user sees the logs appear line by line while the job is running.

When Not to Use SSE

SSE is not always the right choice. Avoid SSE when:

In many backend systems, a combination of technologies is common. For example:

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!