WebSocket-Grundlagen für Backend-Entwickler
Wenn HTTP-Polling nicht reicht: ein praktischer Leitfaden zu WebSocket-Verbindungen, Nachrichtenmustern, Reconnection und Skalierung.

HTTP ist Request-Response. Der Client fragt, der Server antwortet. Für Echtzeit-Funktionen — Live-Benachrichtigungen, kollaboratives Editieren, Chat, Datenstreaming — erzwingt dieses Modell umständliche Workarounds wie Long Polling. WebSockets lösen das mit einer persistenten, bidirektionalen Verbindung, bei der jede Seite jederzeit Daten senden kann.
Wann WebSockets sinnvoll sind
Nicht jede Echtzeit-Funktion braucht WebSockets. Nutze sie, wenn der Server Daten an Clients pushen muss, ohne dass diese fragen.
| Muster | HTTP funktioniert | WebSockets besser |
|---|---|---|
| Dashboard, das alle 30s aktualisiert | ✅ Polling reicht | Übertrieben |
| Live-Chat-Nachrichten | Polling verschwendet Bandbreite | ✅ Sofortige Zustellung |
| Kollaborative Dokumentenbearbeitung | Nicht praktikabel | ✅ Erforderlich |
| Aktienkurs-Ticker | ✅ SSE ist einfacher | ✅ Wenn bidirektional nötig |
| Benachrichtigungsglocke mit Zähler | ✅ Polling oder SSE | ✅ Wenn WS bereits genutzt wird |
Server-Sent Events (SSE) behandeln das Muster "Server pusht, Client hört zu" einfacher als WebSockets. Greife nur dann zu WebSockets, wenn du bidirektionale Kommunikation brauchst.
Grundlegendes Server-Setup
import { WebSocketServer, WebSocket } from "ws";
import { createServer } from "http";
const server = createServer();
const wss = new WebSocketServer({ server });
wss.on("connection", (ws: WebSocket, req) => {
const userId = authenticateFromRequest(req);
if (!userId) {
ws.close(4001, "Unauthorized");
return;
}
console.log(`Client connected: ${userId}`);
ws.on("message", (data) => {
const message = JSON.parse(data.toString());
handleMessage(ws, userId, message);
});
ws.on("close", (code, reason) => {
console.log(`Client disconnected: ${userId}, code: ${code}`);
cleanupConnection(userId);
});
ws.on("error", (error) => {
console.error(`WebSocket error for ${userId}:`, error.message);
});
// Send initial state
ws.send(JSON.stringify({ type: "connected", userId }));
});
server.listen(3001);Authentifiziere immer während des Verbindungs-Handshakes, nicht danach. Eine unauthentifizierte WebSocket-Verbindung ist eine offene Tür.
Design des Nachrichtenprotokolls
Definiere ein typisiertes Nachrichtenprotokoll im Voraus. Ohne das landest du beim Parsen unstrukturierter Blobs und hoffst auf das Beste.
// ❌ Untyped messages — no contract, no validation
ws.send("hello");
ws.send(JSON.stringify({ action: "send", text: "hi" }));
ws.send(JSON.stringify({ type: "msg", body: "hello" }));
// ✅ Typed message protocol — both sides know the contract
type ClientMessage =
| { type: "chat:send"; roomId: string; content: string }
| { type: "chat:typing"; roomId: string }
| { type: "presence:update"; status: "online" | "away" };
type ServerMessage =
| { type: "chat:new"; roomId: string; message: ChatMessage }
| { type: "chat:typing"; roomId: string; userId: string }
| { type: "presence:changed"; userId: string; status: string }
| { type: "error"; code: string; message: string };
function handleMessage(ws: WebSocket, userId: string, msg: ClientMessage) {
switch (msg.type) {
case "chat:send":
broadcastToRoom(msg.roomId, {
type: "chat:new",
roomId: msg.roomId,
message: { userId, content: msg.content, timestamp: Date.now() },
});
break;
case "chat:typing":
broadcastToRoom(msg.roomId, {
type: "chat:typing",
roomId: msg.roomId,
userId,
});
break;
}
}Namespace die Nachrichtentypen mit Doppelpunkten (chat:send, presence:update). Das hält das Protokoll organisiert, während es wächst.
Verbindungsverwaltung
Verfolge verbundene Clients in einer Map für gezieltes Messaging und Aufräumen.
const connections = new Map<string, WebSocket>();
function registerConnection(userId: string, ws: WebSocket) {
// Close existing connection if user reconnects
const existing = connections.get(userId);
if (existing?.readyState === WebSocket.OPEN) {
existing.close(4000, "Replaced by new connection");
}
connections.set(userId, ws);
}
function sendToUser(userId: string, message: ServerMessage) {
const ws = connections.get(userId);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
function broadcastToRoom(roomId: string, message: ServerMessage) {
const members = getRoomMembers(roomId);
const payload = JSON.stringify(message);
for (const userId of members) {
const ws = connections.get(userId);
if (ws?.readyState === WebSocket.OPEN) {
ws.send(payload);
}
}
}Reconnection auf Client-Seite
Verbindungen brechen ab. Netzwerke wechseln. Server starten neu. Der Client muss Reconnection sauber handhaben.
// ✅ Reconnecting WebSocket client with exponential backoff
function createReliableSocket(url: string) {
let ws: WebSocket | null = null;
let retryCount = 0;
const maxRetryDelay = 30000;
function connect() {
ws = new WebSocket(url);
ws.onopen = () => {
retryCount = 0; // Reset backoff on successful connection
console.log("WebSocket connected");
};
ws.onclose = (event) => {
if (event.code === 4001) return; // Auth failure — don't retry
const delay = Math.min(1000 * 2 ** retryCount, maxRetryDelay);
retryCount++;
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(connect, delay);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
handleServerMessage(message);
};
}
connect();
return {
send: (msg: ClientMessage) => {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
},
};
}Exponentieller Backoff verhindert Thundering Herds, wenn der Server neu startet und alle Clients gleichzeitig versuchen, sich neu zu verbinden.
Heartbeats und tote Verbindungen
TCP-Verbindungen können still sterben (Netzwerkwechsel, Laptop-Standby). Heartbeats erkennen tote Verbindungen, damit der Server Ressourcen aufräumen kann.
const HEARTBEAT_INTERVAL = 30000;
const HEARTBEAT_TIMEOUT = 10000;
wss.on("connection", (ws) => {
let isAlive = true;
ws.on("pong", () => {
isAlive = true;
});
const heartbeat = setInterval(() => {
if (!isAlive) {
ws.terminate();
clearInterval(heartbeat);
return;
}
isAlive = false;
ws.ping();
}, HEARTBEAT_INTERVAL);
ws.on("close", () => clearInterval(heartbeat));
});Ohne Heartbeats sammeln sich tote Verbindungen an und verschwenden Server-Speicher. Eine Verbindung, die still stirbt, löst nie das close-Event aus.
Die wichtigsten Punkte
- Nutze WebSockets für bidirektionale Echtzeitkommunikation — SSE ist einfacher für reine Server-Push-Muster
- Authentifiziere während des Handshakes — lehne unauthentifizierte Verbindungen sofort ab
- Definiere ein typisiertes Nachrichtenprotokoll — namespace die Typen mit Doppelpunkten zur Organisation
- Implementiere Reconnection mit exponentiellem Backoff auf dem Client, um Thundering Herds zu vermeiden
- Heartbeat-Pings erkennen tote Verbindungen — ohne sie verlieren veraltete Verbindungen Speicher


