JavaScript Server-Sent Events (EventSource)
JavaScript Server-Sent Events mit der EventSource-API — einseitiger Datenstrom vom Server über HTTP mit automatischer Wiederverbindung und benannten Ereignissen.
Server-Sent Events (SSE) sind ein Standardverfahren, mit dem ein Server kontinuierlich einen einseitigen Textstrom an den Browser über eine einzelne, langlebige HTTP-Verbindung pushen kann. Anstatt dass der Client den Endpunkt wiederholt mit fetch abfragt und fragt „Gibt es etwas Neues?", bleibt die Verbindung offen und der Server schreibt Nachrichten, wann immer er etwas zu senden hat. Der Browser stellt dies über das eingebaute EventSource-Interface bereit, sodass keine externe Bibliothek benötigt wird, um einen Stream zu empfangen.
SSE eignet sich besonders, wenn Aktualisierungen in eine Richtung fließen — vom Server zum Client. Live-Nachrichtenfeeds, Benachrichtigungen, Fortschrittsbalken für Builds oder Uploads, Monitoring-Dashboards und Börsenticker sind allesamt ideale Anwendungsfälle.
Einen Stream mit EventSource empfangen
Das Öffnen eines Streams erfordert nur eine einzige Zeile. Man erstellt einen EventSource, der auf einen Server-Endpunkt zeigt, und hängt dann Handler für die ausgelösten Ereignisse an.
const es = new EventSource('/events');
es.onmessage = (event) => {
console.log('New message:', event.data);
};
es.onopen = () => {
console.log('Connection opened');
};
es.onerror = (error) => {
console.log('Connection error:', error);
};Ein paar Dinge sind es wert, angemerkt zu werden:
- Die Verbindung wird geöffnet, sobald man den
EventSourceinstanziiert. onmessagewird für jede Standard-Nachricht (ohne Namen) ausgelöst, die der Server sendet. Die Nutzlast ist immer ein string und steht alsevent.datazur Verfügung.onopenwird einmalig ausgelöst, wenn die Verbindung hergestellt ist (und erneut nach einer Wiederverbindung).onerrorwird ausgelöst, wenn die Verbindung abbricht oder fehlschlägt. Anders als ein fehlgeschlagenesfetchbedeutet dies nicht zwangsläufig, dass man aufgeben sollte — der Browser versucht in der Regel, die Verbindung selbstständig wiederherzustellen (dazu weiter unten mehr).
Hier ist ein etwas vollständigeres Beispiel, das JSON-Nutzlasten verarbeitet und die Seite aktualisiert:
const es = new EventSource('/notifications');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
const li = document.createElement('li');
li.textContent = data.message;
document.querySelector('#feed').append(li);
};Das Übertragungsformat
Der Server antwortet mit dem Content-Type text/event-stream und schreibt einfachen UTF-8-Text in einem zeilenbasierten Format. Jede Nachricht besteht aus einem Block mit feld: wert-Zeilen, und eine Leerzeile markiert das Ende einer Nachricht.
data: Hello there
data: First line
data: Second line
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
id: 42
data: This message has an id
retry: 10000
data: Reconnect after 10 seconds next timeDie anerkannten Felder sind:
data:— die Nachrichtennutzlast. Hat eine Nachricht mehreredata:-Zeilen, werden diese vor der Übergabe alsevent.datamit einem Zeilenumbruch (\n) verbunden.event:— ein optionaler Ereignisname. Ohne ihn wird die Nachricht anonmessageübergeben.id:— ein optionaler Bezeichner, den der Browser für die Wiederverbindung speichert.retry:— die Wiederverbindungsverzögerung in Millisekunden.
Zeilen, die mit einem Doppelpunkt (:) beginnen, sind Kommentare und werden ignoriert — Server senden sie häufig als Keep-Alive-Pings.
Benannte Ereignisse
Standardmäßig gehen alle Nachrichten an onmessage. Ein Server kann eine Nachricht stattdessen mit einem event:-Feld kennzeichnen, und der Client lauscht mit addEventListener auf diesen spezifischen Namen. Dadurch können verschiedene Arten von Aktualisierungen über dieselbe Verbindung an unterschiedliche Handler weitergeleitet werden.
Bei einem Stream wie diesem:
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
event: newsAlert
data: Markets closed early today
data: a plain default messageWürde man den Client folgendermaßen einrichten:
const es = new EventSource('/market');
es.addEventListener('priceUpdate', (event) => {
const { symbol, price } = JSON.parse(event.data);
console.log(`${symbol} is now ${price}`);
});
es.addEventListener('newsAlert', (event) => {
console.log('News:', event.data);
});
// Messages with no `event:` field still land here.
es.onmessage = (event) => {
console.log('Default message:', event.data);
};Automatische Wiederverbindung
Dies ist die Funktion, die SSE von einer selbst implementierten Streaming-Lösung mit fetch unterscheidet. Wenn die Verbindung abbricht — durch ein unstabiles Netzwerk, einen Server-Neustart oder einen Proxy-Timeout — verbindet sich der Browser automatisch nach einer kurzen Verzögerung neu. Es ist kein eigener Wiederholungs-Loop erforderlich.
Um die Wiederaufnahme zuverlässig zu gestalten, speichert der Browser die zuletzt empfangene id:. Bei der Wiederverbindung sendet er diesen Wert im Request-Header Last-Event-ID zurück, sodass der Server genau dort weitermachen kann, wo der Stream unterbrochen wurde, ohne alles erneut zu übertragen.
GET /events HTTP/1.1
Last-Event-ID: 42
Accept: text/event-streamDer Server kann auch die Verzögerung vor der nächsten Wiederverbindung anpassen, indem er ein retry:-Feld (in Millisekunden) sendet:
retry: 5000
data: The browser will wait 5 seconds before reconnecting if droppedDie automatische Wiederverbindung erfolgt nur, solange die Verbindung als wiederherstellbar gilt. Antwortet der Server auf eine Wiederverbindung mit einem HTTP-Fehler wie 204 No Content oder einem 4xx-Status, behandelt der Browser den Stream als beendet und stellt keine weiteren Verbindungsversuche an.
Verbindungszustand und Schließen
Ein EventSource gibt seinen aktuellen Zustand über die Eigenschaft readyState preis, die drei Konstanten entspricht:
EventSource.CONNECTING(0) — verbindet sich, oder wartet auf Wiederverbindung.EventSource.OPEN(1) — verbunden und empfängt Daten.EventSource.CLOSED(2) — geschlossen und wird sich nicht mehr neu verbinden.
Wenn der Stream nicht mehr benötigt wird, ruft man es.close() auf. Dies beendet die Verbindung sofort, und — wichtig — der Browser wird danach nicht mehr versuchen, sich neu zu verbinden.
const es = new EventSource('/events');
// Stop listening after 30 seconds.
setTimeout(() => {
es.close();
console.log('readyState is now', es.readyState); // 2 (CLOSED)
}, 30000);Cross-Origin-Anfragen und Anmeldeinformationen
Standardmäßig befolgt ein EventSource die Same-Origin-Richtlinie. Um Daten von einer anderen Herkunft zu streamen, muss der Server die entsprechenden CORS-Header senden (wie z. B. Access-Control-Allow-Origin).
Cookies werden bei Cross-Origin-Anfragen nicht gesendet, es sei denn, man entscheidet sich dafür. Man übergibt die Option withCredentials und stellt sicher, dass der Server Anmeldeinformationen in seiner CORS-Konfiguration zulässt:
const es = new EventSource('https://api.example.com/events', {
withCredentials: true,
});Ein Hinweis zur Serverseite
SSE ist hauptsächlich eine Browser-Funktion, aber der Server muss mitspielen. Unabhängig von der verwendeten Sprache ist das Rezept dasselbe: mit Content-Type: text/event-stream antworten, die Verbindung offen halten statt die Antwort zu beenden, und ereignisformatierte Chunks schreiben, sobald Daten verfügbar sind.
Hier ist ein minimales Node.js-Beispiel, das die Grundstruktur zeigt:
import http from 'node:http';
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let id = 0;
const timer = setInterval(() => {
id += 1;
res.write(`id: ${id}\n`);
res.write(`data: The time is ${new Date().toISOString()}\n\n`);
}, 1000);
// Stop the timer when the client disconnects.
req.on('close', () => clearInterval(timer));
}).listen(3000);Jeder Chunk endet mit einer Leerzeile (\n\n), um die Nachricht abzuschließen. Der mit new EventSource('http://localhost:3000') verbundene Client würde eine Aktualisierung pro Sekunde empfangen.
Einschränkungen
SSE überträgt ausschließlich Text (UTF-8) — es gibt keinen Binär-Frame-Typ, daher können keine rohen Bytes wie Bilder oder Audio gestreamt werden, ohne sie vorher zu kodieren. Es ist außerdem streng einseitig: Um Daten an den Server zu senden, wird weiterhin eine normale Anfrage über fetch oder XMLHttpRequest benötigt, oder eine Vollduplex-WebSocket-Verbindung.
Eine weitere praktische Einschränkung: Über HTTP/1.1 begrenzen Browser die Anzahl gleichzeitiger Verbindungen zu einem einzigen Ursprung auf etwa sechs. Da jeder EventSource eine Verbindung offen hält, kann das Öffnen vieler Tabs zur gleichen Website dieses Budget erschöpfen. Bei HTTP/2 ist dies weit weniger ein Problem, da viele Streams auf eine einzige Verbindung gemultiplext werden.
SSE vs. WebSockets
SSE und WebSockets liefern beide Echtzeit-Daten, lösen jedoch unterschiedliche Probleme.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Richtung | Einseitig (Server → Client) | Bidirektional (Vollduplex) |
| Protokoll | Einfaches HTTP | ws:// / wss:// Upgrade |
| Daten | Nur Text (UTF-8) | Text und Binärdaten |
| Wiederverbindung | Automatisch, eingebaut | Selbst implementieren |
| Optimal für | Feeds, Benachrichtigungen, Fortschritt | Chat, Spiele, kollaboratives Bearbeiten |
SSE sollte gewählt werden, wenn der Browser nur lauschen muss — Benachrichtigungen, Live-Feeds, Dashboards, Fortschrittsanzeigen. Die automatische Wiederverbindung, der einfache HTTP-Transport und die kompakte Client-API machen es zum einfachsten Werkzeug für diese Aufgabe. WebSockets sollten gewählt werden, wenn der Client auch häufig senden muss, wenn Binärdaten benötigt werden, oder wenn die Latenz pro Nachricht eine Rolle spielt, wie bei Chat-Apps und Multiplayer-Spielen.
Wenn die Daten bereits hinter einem Promise-basierten asynchronen Ablauf liegen, hält das Kombinieren von SSE-Handlern mit async/await für alle Folge-Anfragen (zum Beispiel das Abrufen vollständiger Details, wenn eine leichtgewichtige Benachrichtigung eintrifft) den Code sauber und lesbar.