Echtzeit-Funktionen mit PHP: WebSockets, SSE & Live-Dashboards
Echtzeit-Funktionen mit PHP: WebSockets, SSE & Live-Dashboards
“Der Kunde will, dass sich das Dashboard automatisch aktualisiert. Ohne Reload.” - Diesen Satz höre ich in fast jedem zweiten Projekt. Die Erwartung an Web-Apps hat sich verändert: Nutzer wollen Live-Daten, sofortige Benachrichtigungen und Dashboards, die sich von selbst aktualisieren.
Das Problem: Klassisches HTTP ist Request-Response. Der Browser fragt, der Server antwortet. Ohne Anfrage passiert nichts. Für Echtzeit brauchen Sie einen Rückkanal - und da gibt es drei grundlegend verschiedene Ansätze.
Für Entscheider: Warum Echtzeit Geld spart
Echtzeit-Funktionen sind kein technisches Spielzeug. Sie lösen konkrete Business-Probleme:
| Problem | Ohne Echtzeit | Mit Echtzeit |
|---|---|---|
| Lager-Dashboard | Mitarbeiter drückt F5, sieht veraltete Bestände | Bestand aktualisiert sich sofort nach Buchung |
| Support-Ticketsystem | Zwei Mitarbeiter bearbeiten dasselbe Ticket | Live-Anzeige “wird gerade bearbeitet von…” |
| Auftragsübersicht | Chef prüft alle 10 Minuten den Status | Push-Benachrichtigung bei Statusänderung |
| Monitoring | Fehler werden erst bei nächster Prüfung entdeckt | Sofort-Alarm bei Grenzwert-Überschreitung |
Noch teurer als die Wartezeit sind Fehler durch veraltete Daten, doppelte Arbeit und falsche Entscheidungen. Ein Mitarbeiter sieht “15 auf Lager”, ein anderer hat gerade 12 davon verbucht. Ein Disponent plant mit Beständen, die seit einer Stunde nicht mehr stimmen. Dazu kommt die reine Zeitverschwendung: Wenn 20 Mitarbeiter jeweils 30x am Tag F5 drücken und jedes Mal 5 Sekunden warten, ist das fast eine Arbeitsstunde täglich - nur fürs Neuladen.
Die drei Ansätze im Vergleich
1. Polling: Der einfachste Ansatz
Der Browser fragt regelmäßig beim Server nach: “Gibt es was Neues?”
// Alle 5 Sekunden den Server fragen
const pollInterval = setInterval(async () => {
const response = await fetch('/api/dashboard-data');
const data = await response.json();
updateDashboard(data);
}, 5000);
// Wichtig: Bei Seitenwechsel oder Component-Unmount aufräumen
// clearInterval(pollInterval);
Vorteile: Trivial zu implementieren, funktioniert mit jedem Server, kein besonderes Setup.
Nachteile: Unnötige Last (99% der Requests liefern “keine Änderung”), Verzögerung bis zum nächsten Intervall, skaliert schlecht (100 Nutzer × alle 5 Sekunden = 20 Requests/Sekunde für nichts).
Wann Polling reicht: Wenn Daten sich selten ändern (alle paar Minuten), wenige gleichzeitige Nutzer (unter 50) und Sie keine Infrastruktur für WebSockets/SSE aufbauen wollen.
2. Server-Sent Events (SSE): Der Einweg-Kanal
Der Server schickt Daten an den Browser, wann immer es etwas Neues gibt. Der Browser hört zu - ohne ständig zu fragen.
// PHP: SSE-Endpoint
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
while (true) {
$data = getLatestUpdates(); // Ihre Datenquelle
if ($data) {
echo "data: " . json_encode($data) . "\n\n";
ob_flush();
flush();
}
sleep(1); // 1 Sekunde warten, dann erneut prüfen
}
// Browser: SSE empfangen
const source = new EventSource('/api/stream');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
updateDashboard(data);
};
source.onerror = () => {
// Reconnect passiert automatisch
console.log('Verbindung unterbrochen, reconnecting...');
};
Wichtig: In echten Projekten braucht ein SSE-Endpoint zusätzlich Heartbeats (regelmäßige Keep-Alive-Nachrichten, damit Proxies die Verbindung nicht schließen) und sauberes Timeout-/Disconnect-Handling. Die Snippets oben zeigen das Grundprinzip, nicht den Production-Code.
Vorteile: Einfach zu implementieren (reines HTTP), automatischer Reconnect eingebaut, nativ in allen modernen Browsern. SSE lässt sich auch mit klassischem PHP umsetzen - allerdings nur sinnvoll für moderate Nutzerzahlen und mit Blick auf blockierte Worker.
Nachteile: Nur Server → Browser (kein Rückkanal), pro SSE-Verbindung blockiert ein PHP-Prozess. SSE funktioniert heute in modernen Browsern gut. Bei älteren Setups mit HTTP/1.1 können parallele Verbindungen pro Domain jedoch begrenzt sein (typisch 6 pro Domain).
Wann SSE die richtige Wahl ist: Live-Dashboards, Benachrichtigungen, Monitoring, Newsfeeds - überall, wo der Server Daten pusht und der Client nur zuhört.
3. WebSockets: Bidirektionale Verbindung
Permanente Vollduplex-Verbindung - Server und Client können gleichzeitig und jederzeit Daten senden und empfangen.
// Browser: WebSocket
const ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
// Authentifizierung senden
ws.send(JSON.stringify({ type: 'auth', token: '...' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleRealtimeUpdate(data);
};
// Daten senden (z.B. Chat-Nachricht)
function sendMessage(text) {
ws.send(JSON.stringify({ type: 'message', text }));
}
Vorteile: Bidirektional (Client ↔ Server), minimaler Overhead nach dem Handshake, ideal für hohe Frequenz (Chat, Gaming, Collaboration).
Nachteile: WebSockets lassen sich im PHP-Umfeld meist nicht sinnvoll mit klassischem Request/Response-Betrieb lösen. In der Praxis kommt dafür fast immer ein separater, langlebiger Prozess oder eigener Server dazu. Komplexere Infrastruktur (Reverse Proxy, Load Balancing), kein automatischer Reconnect (müssen Sie selbst bauen).
Wann WebSockets nötig sind: Chat, kollaboratives Editing (mehrere Nutzer bearbeiten gleichzeitig), Gaming, Echtzeit-Collaboration à la Google Docs.
Entscheidungshilfe
| Kriterium | Polling | SSE | WebSockets |
|---|---|---|---|
| Richtung | Client → Server | Server → Client | Bidirektional |
| Implementierung | Trivial | Einfach | Komplex |
| PHP-kompatibel | Ja | Ja (mit Einschränkungen) | Braucht separaten Server |
| Latenz | Sekunden (Intervall) | Millisekunden | Millisekunden |
| Skalierung | Schlecht (unnötige Last) | Mittel | Gut |
| Browser-Support | Alle | Alle modernen | Alle modernen |
| Reconnect | Eingebaut (nächstes Intervall) | Automatisch | Selbst implementieren |
Meine Faustregel:
- Dashboard, Monitoring, Benachrichtigungen → SSE (einfach, reicht fast immer)
- Chat, Collaboration, Gaming → WebSockets (bidirektional nötig)
- Seltene Updates, wenige Nutzer → Polling (warum komplizierter als nötig?)
Praxis: Live-Dashboard mit SSE und PHP
Ein typisches Szenario: Auftragsübersicht, die sich automatisch aktualisiert, wenn ein neuer Auftrag eingeht oder ein Status sich ändert.
Architektur
Browser Server
│ │
│── GET /api/stream ──────>│ (SSE-Verbindung öffnen)
│ │
│<── data: {neue Auftr.} ──│ (sofort bei Änderung)
│<── data: {Status upd.} ──│ (sofort bei Änderung)
│ │
│ (Verbindung bleibt offen, Server pusht bei Bedarf)
Das PHP-Problem: Blockierende Prozesse
Standard-PHP (Apache mod_php, PHP-FPM) ist für kurze Request-Response-Zyklen gebaut. Eine SSE-Verbindung, die minutenlang offen bleibt, blockiert einen PHP-Worker-Prozess.
Bei 20 gleichzeitigen Dashboard-Nutzern: 20 blockierte PHP-Prozesse. Standard PHP-FPM hat oft nur 10-30 Worker konfiguriert. Das wird eng.
Lösungen:
Die folgenden Angaben sind Daumenregeln, keine harten Architekturgrenzen - die tatsächliche Grenze hängt von Server-Ausstattung, Datenvolumen und Update-Frequenz ab:
| Ansatz | Aufwand | Typischer Einsatz |
|---|---|---|
| PHP-FPM Worker erhöhen | Minimal | Kleine interne Tools |
| Datenbank-Polling im SSE | Gering | Moderate Last, typische Business-Apps |
| Redis Pub/Sub + SSE | Mittel | Größere Multi-User-Setups |
| Separater SSE-Server (Node.js, Go) | Hoch | Hohe Last, mehrere Server |
Für die meisten Business-Apps reicht der einfache Ansatz: PHP-FPM Worker auf 50-100 setzen und im SSE-Endpoint per Datenbank-Polling (alle 1-2 Sekunden) auf Änderungen prüfen. Das ist nicht elegant, aber es funktioniert zuverlässig für interne Tools mit 20-50 gleichzeitigen Nutzern.
Änderungen erkennen: Drei Ansätze
Woher weiß der SSE-Endpoint, dass sich etwas geändert hat?
A) Timestamp-basiert (einfach):
// Letzte Änderung prüfen
$lastCheck = time();
while (true) {
$updates = $db->query(
"SELECT * FROM auftraege WHERE updated_at > to_timestamp($1)",
[$lastCheck]
);
if ($updates) {
echo "data: " . json_encode($updates) . "\n\n";
flush();
$lastCheck = time();
}
sleep(1);
}
Für Entwickler: PostgreSQL LISTEN/NOTIFY
-- 1. Trigger-Funktion anlegen
CREATE OR REPLACE FUNCTION notify_order_change()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('order_updates', json_build_object(
'action', TG_OP,
'order_id', NEW.id
)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 2. Trigger an Tabelle binden
CREATE TRIGGER order_change_trigger
AFTER INSERT OR UPDATE ON auftraege
FOR EACH ROW EXECUTE FUNCTION notify_order_change();
// 3. PHP: Auf Notifications lauschen (SSE-Endpoint)
$db = pg_connect("host=localhost dbname=app");
pg_query($db, "LISTEN order_updates");
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
while (true) {
$result = pg_get_notify($db);
if ($result) {
echo "data: " . $result['payload'] . "\n\n";
ob_flush();
flush();
}
usleep(100000); // 100ms warten
}
Kein Polling gegen die Datenbank nötig - PostgreSQL pusht die Änderung aktiv. Deutlich effizienter, braucht aber eine persistente DB-Verbindung pro SSE-Client.
C) Redis Pub/Sub (für größere Setups):
// Schreibvorgang: Nachricht in Redis publizieren
$redis = new Redis();
$redis->connect('127.0.0.1');
$redis->publish('order_updates', json_encode([
'action' => 'update',
'order_id' => $orderId
]));
Alle SSE-Endpoints subscriben den Redis-Channel. Vorteil gegenüber PostgreSQL NOTIFY: Funktioniert auch über mehrere Server hinweg und entkoppelt die Echtzeit-Kommunikation von der Datenbank. Erfordert aber einen Redis-Server und die PHP-Extension phpredis.
Häufige Fallen
1. SSE hinter Reverse Proxy bricht ab
Nginx buffert Server-Responses standardmäßig. SSE-Streams werden dadurch blockiert, bis der Buffer voll ist.
Lösung: In der Nginx-Config für den SSE-Endpoint:
location /api/stream {
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s; # 24h statt 60s
}
2. Authentifizierung bei SSE
SSE nutzt EventSource - das unterstützt keine Custom-Headers. Sie können keinen Authorization: Bearer-Header mitsenden.
Lösung: Cookie-basierte Authentifizierung (sauberer, weil das Token nicht in der URL und damit nicht in Server-Logs landet) oder Token als Query-Parameter (/api/stream?token=..., einfacher).
// Cookie-basierte Auth im SSE-Endpoint
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit('Unauthorized');
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
// ... SSE-Stream wie oben
3. Reconnect-Logik fehlt bei WebSockets
SSE hat automatischen Reconnect. WebSockets nicht. Wenn die Verbindung abbricht (WLAN-Wechsel, Server-Restart), bleibt der Client stumm.
Minimum:
let shouldReconnect = true;
function connectWS() {
const ws = new WebSocket('wss://...');
ws.onclose = () => {
if (shouldReconnect) {
setTimeout(connectWS, 3000);
}
};
return ws;
}
// Bei Logout: Reconnect verhindern
function logout() {
shouldReconnect = false;
ws.close();
}
Für Production-Systeme empfiehlt sich zusätzlich exponentielles Backoff (3s, 6s, 12s…) und ein maximaler Retry-Count, damit der Client nicht endlos gegen einen toten Server anläuft.
4. Zu viele Updates fluten das Frontend
Wenn sich Daten 50x pro Sekunde ändern, müssen Sie das Frontend nicht 50x pro Sekunde updaten. Das führt zu Ruckeln und hoher CPU-Last im Browser.
Lösung: Throttling - Daten sammeln, gebündelt rendern:
let pendingUpdates = [];
let throttleTimer = null;
source.onmessage = (event) => {
pendingUpdates.push(JSON.parse(event.data));
if (!throttleTimer) {
throttleTimer = setTimeout(() => {
updateDashboard(pendingUpdates);
pendingUpdates = [];
throttleTimer = null;
}, 500); // Maximal alle 500ms ein DOM-Update
}
};
Wann sich der Aufwand lohnt - und wann nicht
Echtzeit lohnt sich:
- Mehrere Nutzer arbeiten mit denselben Daten (Kollisionsvermeidung)
- Zeitkritische Informationen (Monitoring, Alerts, Auftragseingang)
- Nutzer lassen die App den ganzen Tag offen (Dashboards, Support-Tools)
Echtzeit ist Overkill:
- Daten ändern sich selten (einmal pro Stunde reicht ein manueller Refresh)
- Nur ein Nutzer arbeitet im System (keine Kollisionen möglich)
- Informationen sind nicht zeitkritisch (Monatsreport braucht kein Live-Update)
Mein pragmatischer Ansatz für die meisten Business-Apps: SSE für Live-Updates im Dashboard, klassisches HTTP für alles andere. Keine extra Infrastruktur, kein WebSocket-Server, kein Redis - solange die Nutzerzahl unter 100 bleibt.
Checkliste: Echtzeit in Ihrer Web-App
- Brauchen Sie wirklich Echtzeit? Oder reicht ein Refresh-Button?
- Welche Richtung? Server → Client (SSE) oder bidirektional (WebSocket)?
- Wie viele gleichzeitige Nutzer? Unter 50: SSE mit PHP. Über 100: separater Server.
- Nginx/Reverse Proxy konfiguriert? Buffering aus für SSE-Endpoints.
- Reconnect-Logik vorhanden? Besonders bei WebSockets (mit Backoff und max. Retries).
- Throttling im Frontend? Nicht jedes Update muss sofort gerendert werden.
- Authentifizierung gelöst? SSE: Cookie oder Token-Parameter. WebSocket: im Handshake.
- Error-Handling eingebaut? SSE und WebSocket-Verbindungen können jederzeit fehlschlagen.
- Load-Testing durchgeführt? Wie viele gleichzeitige SSE-Verbindungen hält Ihr Server?
- Monitoring vorhanden? Offene SSE/WebSocket-Verbindungen tracken, tote Verbindungen erkennen.
Unsicher, ob SSE oder WebSockets für Ihr Projekt passen? Ich schaue mir Ihre Anforderungen an und empfehle den einfachsten Ansatz, der funktioniert - kostenloses Erstgespräch anfragen.
Über Carola Schulte
Software-Architektin mit 25+ Jahren Erfahrung. Spezialisiert auf robuste Business-Apps mit PHP/PostgreSQL, Security-by-Design und DSGVO-konforme Systeme. 1,8M+ Lines of Code in Produktion.
Projekt im Kopf?
Lassen Sie uns besprechen, wie ich Ihre Anforderungen umsetzen kann – kostenlos und unverbindlich.
Kostenloses Erstgespräch