Real-Time-Anwendungen sind 2026 kein Luxus mehr — sie sind Standard.
Nutzer erwarten, dass Nachrichten sofort ankommen, Dashboards live aktualisieren und kollaborative Dokumente ohne Reload synchronisieren.
WebSockets sind das Fundament dahinter, und Claude Code kann den gesamten Stack —
Server, Client, Auth, Rooms und Redis-Scaling — in Minuten aufsetzen.
In diesem Artikel zeigen wir, wie du mit Claude Code produktionsreife WebSocket-Anwendungen entwickelst:
vom nativen WS-Protokoll bis zum horizontal skalierten Multi-Server-Setup mit Redis Adapter.
< 1ms
Latenz (LAN)
10k+
Concurrent Connections
100%
Bidirektional
1. WebSocket Grundlagen & Protokoll
Das WebSocket-Protokoll (RFC 6455) ermöglicht eine persistente, bidirektionale TCP-Verbindung
zwischen Browser und Server. Anders als HTTP sendet nicht nur der Client Anfragen —
der Server kann jederzeit Daten pushen, ohne dass der Client pollt.
HTTP-Upgrade Handshake
Eine WebSocket-Verbindung beginnt als normaler HTTP-Request. Der Client sendet einen
Upgrade: websocket-Header, der Server antwortet mit 101 Switching Protocols.
Danach läuft die gesamte Kommunikation über das WS-Binärprotokoll (Frames).
HTTP Upgrade Handshake (vereinfacht)
// Client → Server
GET /socket HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
// Server → Client
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Natives WebSocket API im Browser
client.ts — Natives Browser WebSocket
const ws = new WebSocket('wss://api.example.com/socket');
ws.onopen = () => {
console.log('Verbindung hergestellt');
ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Empfangen:', data);
};
ws.onclose = (event) => {
console.log(`Verbunden getrennt: Code ${event.code}`);
// Auto-Reconnect nach 3 Sekunden
setTimeout(() => reconnect(), 3000);
};
ws.onerror = (error) => console.error('WS Fehler:', error);
// Ping/Pong Keepalive
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30_000);
Native WS-Library (Node.js Server)
server.ts — ws Library
import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http';
const httpServer = createServer();
const wss = new WebSocketServer({ server: httpServer });
wss.on('connection', (socket, request) => {
const clientIp = request.socket.remoteAddress;
console.log(`Neuer Client: ${clientIp}`);
socket.on('message', (rawData) => {
try {
const msg = JSON.parse(rawData.toString());
if (msg.type === 'ping') {
socket.send(JSON.stringify({ type: 'pong', ts: Date.now() }));
return;
}
// Broadcast an alle anderen Clients
wss.clients.forEach((client) => {
if (client !== socket && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg));
}
});
} catch (e) {
socket.send(JSON.stringify({ error: 'Ungültige JSON-Payload' }));
}
});
socket.on('close', () => console.log('Client getrennt'));
});
httpServer.listen(3000, () => console.log('WS-Server auf Port 3000'));
Vergleich: WebSockets vs. SSE vs. Long-Polling
| Merkmal |
WebSockets |
SSE |
Long-Polling |
| Bidirektional |
Ja |
Nein (nur Server→Client) |
Eingeschränkt |
| Latenz |
Sehr niedrig |
Niedrig |
Hoch |
| Overhead |
Minimal (Frames) |
Mittel |
Hoch (HTTP-Headers) |
| Browser-Support |
Exzellent |
Gut (kein IE) |
Universal |
| Load Balancer |
Sticky Sessions nötig |
Sticky Sessions |
Standard HTTP |
Empfehlung: Für Chat, Spiele und Live-Kollaboration → WebSockets. Für Notifications und Feeds → SSE. Long-Polling nur als Legacy-Fallback oder bei strikten Firewall-Regeln.
2. Socket.io Setup: Server & Client
Socket.io ist die produktionsreife Abstraktionsschicht über WebSockets.
Es bietet automatisches Reconnect, Namespaces, Rooms, Middleware-Support und
einen transparenten Fallback auf Long-Polling (z.B. in restriktiven Unternehmensnetzwerken).
Installation
npm install socket.io socket.io-client
npm install -D @types/node typescript tsx
Server Setup
server/index.ts
import { createServer } from 'http';
import { Server, Socket } from 'socket.io';
interface ServerToClientEvents {
message: (payload: { from: string; text: string; ts: number }) => void;
userJoined: (username: string) => void;
userLeft: (username: string) => void;
}
interface ClientToServerEvents {
sendMessage: (text: string) => void;
joinRoom: (room: string) => void;
}
interface SocketData {
userId: string;
username: string;
}
const httpServer = createServer();
const io = new Server<ClientToServerEvents, ServerToClientEvents, {}, SocketData>(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173',
methods: ['GET', 'POST'],
},
pingTimeout: 60_000,
pingInterval: 25_000,
});
io.on('connection', (socket: Socket) => {
console.log(`Client verbunden: ${socket.id}`);
socket.on('sendMessage', (text) => {
const payload = {
from: socket.data.username || 'Anonym',
text,
ts: Date.now(),
};
// An alle in allen Rooms dieses Sockets senden
io.emit('message', payload);
});
socket.on('disconnect', () => {
console.log(`Client getrennt: ${socket.id}`);
});
});
httpServer.listen(3001, () => {
console.log('Socket.io Server läuft auf :3001');
});
Client Setup (React + TypeScript)
client/useSocket.ts
import { useEffect, useRef, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocket(url: string) {
const socketRef = useRef<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState<{ from: string; text: string }[]>([]);
useEffect(() => {
socketRef.current = io(url, {
autoConnect: true,
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 10_000,
reconnectionAttempts: Infinity,
});
const socket = socketRef.current;
socket.on('connect', () => {
setIsConnected(true);
console.log(`Verbunden: ${socket.id}`);
});
socket.on('disconnect', () => setIsConnected(false));
socket.on('message', (payload) => {
setMessages((prev) => [...prev, payload]);
});
return () => { socket.disconnect(); };
}, [url]);
const sendMessage = (text: string) => {
socketRef.current?.emit('sendMessage', text);
};
return { isConnected, messages, sendMessage };
}
Claude Code Prompt
Socket.io Projekt bootstrap
Prompt: "Erstelle ein Socket.io-Projekt mit TypeScript, Express-Backend, React-Frontend und vollständiger Type-Safety für Server- und Client-Events. Inkl. CORS-Konfiguration und Reconnect-Logik."
3. Rooms & Broadcasting
Rooms sind Socketio's Mechanismus, um Clients in logische Gruppen einzuteilen.
Ein Client kann in mehreren Rooms gleichzeitig sein. Nachrichten können gezielt an einzelne Rooms,
an alle außer dem Sender oder an alle Verbundenen gesendet werden.
Room Management
Emit-Varianten im Überblick
socket.join('room-name') — Client tritt Room bei
socket.leave('room-name') — Client verlässt Room
io.to('room-name').emit('event', data) — An alle im Room
socket.broadcast.emit('event', data) — An alle außer Sender
socket.to('room-name').emit('event', data) — An Room, außer Sender
io.emit('event', data) — An alle verbundenen Clients
socket.to(targetSocketId).emit('event', data) — Private Nachricht
Rooms implementieren
server/rooms.ts
import { Server, Socket } from 'socket.io';
interface JoinRoomPayload {
room: string;
username: string;
}
interface MessagePayload {
room: string;
text: string;
}
export function registerRoomHandlers(io: Server, socket: Socket) {
// Room beitreten
socket.on('joinRoom', async ({ room, username }: JoinRoomPayload) => {
await socket.join(room);
socket.data.username = username;
socket.data.currentRoom = room;
// Alle anderen im Room informieren
socket.to(room).emit('userJoined', {
username,
socketId: socket.id,
ts: Date.now(),
});
// Aktuelle Room-Mitglieder an neuen User senden
const sockets = await io.in(room).fetchSockets();
const members = sockets.map((s) => ({
id: s.id,
username: s.data.username,
}));
socket.emit('roomMembers', members);
console.log(`${username} trat Room "${room}" bei (${sockets.length} Mitglieder)`);
});
// Nachricht an Room senden
socket.on('roomMessage', ({ room, text }: MessagePayload) => {
if (!socket.rooms.has(room)) {
socket.emit('error', { msg: 'Du bist nicht in diesem Room' });
return;
}
io.to(room).emit('message', {
from: socket.data.username,
text,
room,
ts: Date.now(),
});
});
// Private Nachricht (1:1)
socket.on('privateMessage', ({ toSocketId, text }: { toSocketId: string; text: string }) => {
socket.to(toSocketId).emit('privateMessage', {
from: socket.data.username,
text,
ts: Date.now(),
});
});
// Room verlassen
socket.on('leaveRoom', async (room: string) => {
await socket.leave(room);
io.to(room).emit('userLeft', {
username: socket.data.username,
ts: Date.now(),
});
});
}
4. Authentifizierung mit JWT
Produktive WebSocket-Apps brauchen sichere Authentifizierung.
Der empfohlene Ansatz: JWT im socket.handshake.auth-Objekt übergeben
und in einer Socket.io-Middleware validieren. Fehlende oder ungültige Tokens führen zum Disconnect.
JWT Auth Middleware (Server)
server/middleware/auth.ts
import { Server, Socket } from 'socket.io';
import jwt from 'jsonwebtoken';
interface JwtPayload {
sub: string;
username: string;
roles: string[];
}
const JWT_SECRET = process.env.JWT_SECRET!;
export function applyAuthMiddleware(io: Server) {
io.use(async (socket: Socket, next) => {
const token = socket.handshake.auth?.token as string | undefined;
if (!token) {
return next(new Error('Authentifizierung erforderlich'));
}
try {
const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
// User-Daten am Socket speichern
socket.data.userId = payload.sub;
socket.data.username = payload.username;
socket.data.roles = payload.roles;
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
return next(new Error('Token abgelaufen — bitte neu einloggen'));
}
return next(new Error('Ungültiges Token'));
}
});
}
Token im Client übergeben
client/socket.ts
import { io } from 'socket.io-client';
function createAuthenticatedSocket(token: string) {
return io('https://api.example.com', {
auth: { token },
reconnection: true,
// Token bei Reconnect erneuern
reconnectionDelay: 1000,
});
}
// Bei Token-Ablauf: neuen Token holen und reconnecten
const socket = createAuthenticatedSocket(localStorage.getItem('auth_token') || '');
socket.on('connect_error', async (err) => {
if (err.message.includes('abgelaufen')) {
// Refresh Token → neues JWT holen
const newToken = await refreshAuthToken();
localStorage.setItem('auth_token', newToken);
socket.auth = { token: newToken };
socket.connect();
}
});
Role-Based Guards
server/guards.ts
import { Socket } from 'socket.io';
export function requireRole(role: string) {
return (socket: Socket, next: (err?: Error) => void) => {
const roles: string[] = socket.data.roles || [];
if (!roles.includes(role)) {
socket.disconnect();
return;
}
next();
};
}
// Usage im Event-Handler:
io.on('connection', (socket) => {
socket.on('adminBroadcast', (msg) => {
if (!socket.data.roles?.includes('admin')) {
socket.emit('error', { msg: 'Keine Berechtigung' });
return;
}
io.emit('systemMessage', msg);
});
});
Sicherheitshinweis: JWT-Secret niemals im Frontend exponieren. Tokens mit kurzer Lebensdauer (15 min) + Refresh-Token-Mechanismus nutzen. Sensitive Events immer serverseitig validieren — Client-seitige Checks sind nur UX, keine Sicherheit.
5. Real-Time Patterns: Chat, Dashboard & Kollaboration
Pattern 1
Real-Time Chat mit Message-History
server/chat.ts
import { Server, Socket } from 'socket.io';
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
const MSG_TTL = 60 * 60 * 24 * 7; // 7 Tage
interface ChatMessage {
id: string;
room: string;
from: string;
text: string;
ts: number;
}
export function registerChatHandlers(io: Server, socket: Socket) {
// Letzte 50 Nachrichten beim Join laden
socket.on('joinRoom', async (room: string) => {
await socket.join(room);
const history = await redis.lrange(`chat:${room}:messages`, -50, -1);
const parsed: ChatMessage[] = history.map((m) => JSON.parse(m));
socket.emit('messageHistory', parsed);
});
socket.on('sendMessage', async ({ room, text }: { room: string; text: string }) => {
const msg: ChatMessage = {
id: crypto.randomUUID(),
room,
from: socket.data.username,
text: text.slice(0, 2000), // Längen-Limit
ts: Date.now(),
};
// In Redis persistieren
const key = `chat:${room}:messages`;
await redis.rpush(key, JSON.stringify(msg));
await redis.expire(key, MSG_TTL);
// An alle im Room broadcasten
io.to(room).emit('message', msg);
});
// Typing-Indicator
socket.on('typing', (room: string) => {
socket.to(room).emit('userTyping', { username: socket.data.username });
});
}
Pattern 2
Live-Collaboration: Cursor-Tracking
server/collaboration.ts
interface CursorPosition {
userId: string;
username: string;
x: number;
y: number;
color: string;
}
const USER_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6'];
const cursors = new Map<string, CursorPosition>();
export function registerCollabHandlers(io: Server, socket: Socket) {
const color = USER_COLORS[cursors.size % USER_COLORS.length];
socket.on('joinDocument', (docId: string) => {
socket.join(`doc:${docId}`);
// Bestehende Cursors an neuen User senden
socket.emit('cursors', Array.from(cursors.values()));
});
socket.on('cursorMove', ({ docId, x, y }: { docId: string; x: number; y: number }) => {
const position: CursorPosition = {
userId: socket.data.userId,
username: socket.data.username,
x, y, color,
};
cursors.set(socket.id, position);
// Throttled: nur alle 50ms broadcasten (serverseitig)
socket.to(`doc:${docId}`).emit('cursorUpdate', position);
});
socket.on('disconnect', () => {
cursors.delete(socket.id);
io.emit('cursorRemoved', socket.data.userId);
});
}
Pattern 3
Live-Dashboard: Server-Metriken in Echtzeit
server/metrics.ts
import os from 'os';
import { Server } from 'socket.io';
interface SystemMetrics {
cpuLoad: number[];
memUsed: number;
memTotal: number;
uptime: number;
connections: number;
ts: number;
}
export function startMetricsBroadcast(io: Server) {
setInterval(async () => {
const sockets = await io.fetchSockets();
const metrics: SystemMetrics = {
cpuLoad: os.loadavg(),
memUsed: process.memoryUsage().heapUsed,
memTotal: os.totalmem(),
uptime: process.uptime(),
connections: sockets.length,
ts: Date.now(),
};
// Nur an Dashboard-Room senden (Admin-only)
io.to('dashboard').emit('metrics', metrics);
}, 2000); // Alle 2 Sekunden
}
// Client: Dashboard abonnieren
const socket = io('https://api.example.com', { auth: { token } });
socket.emit('joinRoom', 'dashboard');
socket.on('metrics', (data: SystemMetrics) => {
updateChart(data); // z.B. Chart.js oder Recharts
});
6. Scaling & Production
Ein einzelner Node.js-Prozess kommt an seine Grenzen. Für produktive Anwendungen mit
horizontalem Scaling (mehrere Server-Instanzen hinter einem Load Balancer)
braucht Socket.io einen Redis Adapter, der Events zwischen den Instanzen synchronisiert.
Skalierung
Das Problem ohne Adapter
Client A ist auf Server 1, Client B auf Server 2. Client A sendet eine Nachricht → nur Clients auf Server 1 empfangen sie. Client B sieht nichts. Der Redis Adapter löst das durch Pub/Sub zwischen den Instanzen.
Redis Adapter Setup
server/redis-adapter.ts
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export async function setupRedisAdapter(io: Server) {
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
console.log('Redis Adapter aktiv — Multi-Instance Scaling möglich');
// Graceful Shutdown
process.on('SIGTERM', async () => {
await Promise.all([pubClient.quit(), subClient.quit()]);
io.close();
});
}
Nginx: Sticky Sessions & WebSocket Proxy
nginx.conf
upstream socketio_backend {
# Sticky Sessions via IP-Hash (alternativ: least_conn)
ip_hash;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 443 ssl http2;
server_name api.example.com;
location /socket.io/ {
proxy_pass http://socketio_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}
Socket.io Admin UI
server/admin.ts
import { Server } from 'socket.io';
import { instrument } from '@socket.io/admin-ui';
import bcrypt from 'bcryptjs';
export function enableAdminUI(io: Server) {
instrument(io, {
auth: {
type: 'basic',
username: 'admin',
password: bcrypt.hashSync(process.env.ADMIN_PASSWORD!, 10),
},
readonly: false,
namespaceName: '/admin',
});
console.log('Socket.io Admin UI: https://admin.socket.io');
}
Production Checklist
Vor dem Go-Live
- Redis Adapter konfiguriert für Multi-Instance
- Sticky Sessions im Load Balancer aktiviert (ip_hash oder Cookie)
- JWT-Auth auf alle Sensitive Events
- Rate Limiting pro Socket (z.B. socket.io-rate-limiter)
- Payload-Größe begrenzen (
maxHttpBufferSize: 1e6)
- CORS explizit konfiguriert (keine Wildcard in Prod)
- Prometheus-Metriken via
@socket.io/prometheus-metrics
- Graceful Shutdown: SIGTERM → io.close() → Redis-Verbindungen trennen
Horizontal Scaling: PM2 Cluster Mode
ecosystem.config.js
module.exports = {
apps: [{
name: 'websocket-server',
script: 'dist/index.js',
instances: 'max', // Alle CPU-Kerne nutzen
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
REDIS_URL: 'redis://localhost:6379',
},
watch: false,
max_restarts: 10,
restart_delay: 4000,
}]
};
Claude Code und WebSockets: Mit dem richtigen Prompt generiert Claude Code den kompletten Stack —
Socket.io Server mit Auth-Middleware, Redis Adapter, React-Hooks für den Client, Nginx-Konfiguration und PM2-Setup —
in einem einzigen Session. Iteriere mit natürlichsprachigen Prompts statt stundenlanger Dokumentation.
Real-Time Apps mit Claude Code bauen
Starte kostenlos und lass Claude Code deinen WebSocket-Stack generieren —
von der ersten Verbindung bis zum Redis-Adapter in Production.
Kostenlos starten →
Kein Kreditkarte erforderlich • Sofort starten