Echtzeit & WebSockets

Socket.io mit Claude Code:
Echtzeit-Apps 2026

Rooms, Namespaces, Acknowledgements, Redis Adapter, Auth-Middleware und React-Integration — der vollständige Guide für skalierbare Echtzeit-Apps mit Node.js.

📅 6. Mai 2026 ⏱ 11 min Lesezeit ✍ SpockyMagicAI
Socket.io Rooms Redis React

Warum Socket.io 2026?

WebSockets sind Standard — aber Socket.io löst die echten Probleme: automatisches Reconnect, Room-Management, Namespace-Isolation und horizontales Scaling mit Redis. Claude Code generiert produktionsreife Socket.io-Architekturen inklusive TypeScript-Typen, Auth-Middleware und React-Hooks — in Minuten statt Stunden.

1. Socket.io Setup — io() Server, CORS, TypeScript-Typen

Der Einstieg in Socket.io beginnt mit dem Server-Setup. Claude Code richtet io() direkt auf einem bestehenden HTTP/Express-Server ein und konfiguriert CORS für Production-Umgebungen korrekt.

Installation und Basis-Setup

# Installation
npm install socket.io
npm install -D @types/socket.io typescript
// server.ts — io() auf Express-Server
import express from 'express';
import { createServer } from 'http';
import { Server, Socket } from 'socket.io';

const app = express();
const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.NODE_ENV === 'production'
      ? ['https://agentic-movers.com', 'https://app.agentic-movers.com']
      : '*',
    methods: ['GET', 'POST'],
    credentials: true,
  },
  transports: ['websocket', 'polling'],
  pingTimeout: 60000,
  pingInterval: 25000,
});

TypeScript-Typen für Events

Claude Code generiert typsichere Event-Interfaces — kein any mehr in Socket-Callbacks:

// types/socket.ts — Event-Interfaces
interface ServerToClientEvents {
  message: (data: { id: string; text: string; userId: string; ts: number }) => void;
  userJoined: (data: { userId: string; room: string }) => void;
  error: (err: { code: string; message: string }) => void;
}

interface ClientToServerEvents {
  sendMessage: (payload: { room: string; text: string }, cb: (ack: AckResponse) => void) => void;
  joinRoom: (room: string, cb: (ack: AckResponse) => void) => void;
}

interface AckResponse {
  success: boolean;
  error?: string;
}

type TypedServer = Server<ClientToServerEvents, ServerToClientEvents>;
type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>;

Event-Handler registrieren

// io.on — Connection-Handler
io.on('connection', (socket: TypedSocket) => {
  console.log(`Client connected: ${socket.id}`);

  socket.on('sendMessage', async (payload, callback) => {
    const message = { id: uuid(), ...payload, ts: Date.now() };
    io.to(payload.room).emit('message', message);
    callback({ success: true });
  });

  socket.on('disconnect', (reason) => {
    console.log(`Disconnected: ${socket.id} — ${reason}`);
  });
});
Claude Code Tipp

Prompt-Vorlage: "Erstelle einen typsicheren Socket.io-Server mit Express, CORS für Production und Event-Interfaces für ChatMessage, UserJoin und Error-Events." Claude Code generiert direkt die korrekten Generic-Parameter für Server<C, S>.


2. Rooms und Namespaces — Isolation und dynamische Gruppen

Rooms gruppieren Sockets logisch; Namespaces schaffen vollständig getrennte Kommunikationskanäle. Claude Code kombiniert beide Konzepte für Chat-Apps, Live-Dashboards und Multi-Tenant-Systeme.

socket.join / leave und io.to().emit

// Rooms — join, leave, broadcast
socket.on('joinRoom', async (room, callback) => {
  try {
    // Validierung: existiert der Room?
    const allowed = await checkRoomAccess(socket.data.userId, room);
    if (!allowed) {
      return callback({ success: false, error: 'Access denied' });
    }

    await socket.join(room);
    io.to(room).emit('userJoined', { userId: socket.data.userId, room });
    callback({ success: true });
  } catch (err) {
    callback({ success: false, error: 'Server error' });
  }
});

// Aus Room entfernen beim Disconnect
socket.on('disconnect', () => {
  socket.rooms.forEach(async (room) => {
    await socket.leave(room);
    io.to(room).emit('userLeft', { userId: socket.data.userId });
  });
});

Namespace-Isolation

// Separate Namespaces für verschiedene Features
const chatNs = io.of('/chat');
const dashboardNs = io.of('/dashboard');
const gameNs = io.of('/game');

chatNs.on('connection', (socket) => {
  console.log(`Chat namespace: ${socket.id}`);
  // Eigene Middleware, eigene Events, eigene Rooms
});

dashboardNs.on('connection', (socket) => {
  // Pusht Live-Metriken — völlig isoliert von Chat-Events
  const interval = setInterval(() => {
    socket.emit('metrics', await getSystemMetrics());
  }, 5000);

  socket.on('disconnect', () => clearInterval(interval));
});

Dynamische Rooms für Multiplayer

// Game-Rooms dynamisch erstellen
const activeRooms = new Map<string, GameRoom>();

gameNs.on('connection', (socket) => {
  socket.on('createRoom', (config: GameConfig, cb) => {
    const roomId = generateRoomId();
    activeRooms.set(roomId, { id: roomId, config: config, players: [socket.id] });
    socket.join(roomId);
    cb({ roomId });
  });

  socket.on('joinGame', (roomId: string, cb) => {
    const room = activeRooms.get(roomId);
    if (!room || room.players.length >= room.config.maxPlayers) {
      return cb({ success: false, error: 'Room full or not found' });
    }
    room.players.push(socket.id);
    socket.join(roomId);
    gameNs.to(roomId).emit('playerJoined', { playerId: socket.id });
    cb({ success: true });
  });
});
Best Practice

Namespace = Feature-Trennung (Chat vs. Dashboard vs. Game). Room = Gruppen-Broadcast innerhalb eines Namespace. Claude Code erstellt beide Ebenen korrekt mit eigener Middleware-Chain pro Namespace.


3. Acknowledgements und Fehlerbehandlung

Socket.io Acknowledgements sind Callbacks, die der Server nach Event-Verarbeitung aufruft — ähnlich wie HTTP-Response-Status. Claude Code implementiert timeout-sichere Acks mit try/catch und Fehler-Propagation.

Server-seitige Ack mit Fehlerbehandlung

// Robuster sendMessage-Handler mit Ack
socket.on('sendMessage', async (payload, callback) => {
  if (typeof callback !== 'function') return; // Guard: Ack vorhanden?

  try {
    // Validierung
    if (!payload.text?.trim()) {
      return callback({ success: false, error: 'Message cannot be empty' });
    }
    if (payload.text.length > 2000) {
      return callback({ success: false, error: 'Message too long' });
    }

    // Persistierung
    const saved = await db.saveMessage({
      room: payload.room,
      text: payload.text,
      userId: socket.data.userId,
    });

    // Broadcast an Room
    io.to(payload.room).emit('message', saved);
    callback({ success: true, messageId: saved.id });

  } catch (err) {
    console.error('sendMessage error:', err);
    callback({ success: false, error: 'Internal server error' });
  }
});

Client-seitiges Emit mit Timeout

// Client: emit mit Timeout (Socket.io 4.4+)
try {
  const response = await socket
    .timeout(5000) // 5 Sekunden Timeout
    .emitWithAck('sendMessage', { room: 'general', text: message });

  if (!response.success) {
    showError(response.error);
  }
} catch (err) {
  // Timeout oder Connection-Error
  if (err.message.includes('timeout')) {
    showError('Server antwortet nicht — bitte erneut versuchen');
  } else {
    showError('Verbindungsfehler');
  }
}

Globaler Error-Handler

// Global error handler für alle Sockets
io.on('connection', (socket) => {
  socket.use(([event, ...args], next) => {
    const callback = args[args.length - 1];
    if (typeof callback === 'function') {
      const wrappedCallback = async (...cbArgs: unknown[]) => {
        try {
          await callback(...cbArgs);
        } catch (err) {
          socket.emit('error', { code: 'HANDLER_ERROR', message: 'Unhandled error' });
        }
      };
      args[args.length - 1] = wrappedCallback;
    }
    next();
  });
});

4. Redis Adapter — Horizontales Scaling mit mehreren Instanzen

Sobald deine App auf mehreren Node.js-Prozessen oder Servern läuft, müssen Events zwischen Instanzen synchronisiert werden. Der @socket.io/redis-adapter löst das Problem — Claude Code konfiguriert ihn mit ioredis für Production.

Wichtig für Production

Ohne Redis Adapter erreichen Broadcasts von Instanz A nur Clients auf Instanz A — Clients auf Instanz B bekommen nichts. Bei einem einzelnen Prozess ist kein Adapter nötig.

Installation und Setup

# Redis Adapter + ioredis
npm install @socket.io/redis-adapter ioredis
// redis-adapter.ts
import { createAdapter } from '@socket.io/redis-adapter';
import { Redis } from 'ioredis';

const pubClient = new Redis({
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  lazyConnect: true,
  retryStrategy: (times) => Math.min(times * 100, 3000),
  maxRetriesPerRequest: 3,
});

const subClient = pubClient.duplicate();

// Verbinden und Adapter setzen
await Promise.all([pubClient.connect(), subClient.connect()]);

io.adapter(createAdapter(pubClient, subClient));
console.log('Redis adapter connected ✓');

Redis Healthcheck und Fehlerbehandlung

// Redis connection error handling
pubClient.on('error', (err) => {
  console.error('Redis pub error:', err);
  // Graceful: Socket.io fällt auf lokalen Adapter zurück
});

subClient.on('error', (err) => {
  console.error('Redis sub error:', err);
});

// Health-Endpoint für Load Balancer
app.get('/health', async (req, res) => {
  const redisOk = pubClient.status === 'ready';
  const sockets = await io.sockets().fetchSockets();

  res.json({
    status: 'ok',
    redis: redisOk ? 'connected' : 'disconnected',
    connections: sockets.length,
    uptime: process.uptime(),
  });
});

Multiple Instanzen mit PM2

// ecosystem.config.js — 4 Instanzen hinter Nginx
module.exports = {
  apps: [{
    name: 'socket-server',
    script: 'dist/server.js',
    instances: 4,         // 4 CPU-Kerne
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      REDIS_HOST: '127.0.0.1',
      REDIS_PORT: '6379',
    },
  }],
};
Claude Code Prompt

"Konfiguriere @socket.io/redis-adapter mit ioredis, Retry-Strategie, Health-Endpoint und PM2-Cluster-Config für 4 Instanzen." Claude Code generiert das komplette Setup inkl. Graceful Shutdown und Signal-Handling.


5. Auth-Middleware — JWT-Validierung und Connection-Security

Ohne Auth können beliebige Clients joinen. Claude Code implementiert socket.handshake.auth-basierte JWT-Validierung als Connection-Middleware — unsichere Verbindungen werden vor dem ersten Event abgelehnt.

JWT Auth-Middleware

// middleware/socketAuth.ts
import jwt from 'jsonwebtoken';
import { ExtendedError } from 'socket.io/dist/namespace';

interface JwtPayload {
  userId: string;
  email: string;
  roles: string[];
  exp: number;
}

export function socketAuthMiddleware(
  socket: TypedSocket,
  next: (err?: ExtendedError) => void
) {
  const token = socket.handshake.auth.token
    || socket.handshake.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return next(new Error('Authentication required'));
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;

    // User-Daten am Socket speichern
    socket.data.userId = payload.userId;
    socket.data.email = payload.email;
    socket.data.roles = payload.roles;

    next(); // Verbindung erlauben
  } catch (err) {
    if (err instanceof jwt.TokenExpiredError) {
      return next(new Error('Token expired'));
    }
    return next(new Error('Invalid token'));
  }
}

Middleware registrieren und Rate Limiting

// Middleware-Chain auf io und Namespaces
io.use(socketAuthMiddleware);
io.use(socketRateLimitMiddleware);

function socketRateLimitMiddleware(socket, next) {
  const key = socket.handshake.address;
  const count = rateLimitMap.get(key) || 0;

  if (count >= 10) { // max 10 Verbindungen/IP/min
    return next(new Error('Too many connections'));
  }
  rateLimitMap.set(key, count + 1);
  setTimeout(() => rateLimitMap.set(key, (rateLimitMap.get(key) || 1) - 1), 60000);
  next();
}

// Disconnect-Handling mit Cleanup
io.on('connection', (socket) => {
  // socket.data.userId ist jetzt immer gesetzt
  console.log(`Authenticated: ${socket.data.userId}`);

  socket.on('disconnect', async (reason) => {
    await updateUserPresence(socket.data.userId, 'offline');
    await cleanupUserRooms(socket.data.userId);
  });
});
Security-Hinweis

JWT-Secret NIEMALS im Code — immer aus process.env.JWT_SECRET. Token-Ablauf validieren und Refresh-Token-Mechanismus getrennt von Socket-Events implementieren. Claude Code fügt diese Hinweise automatisch als Kommentare hinzu.


6. React-Client-Integration — useSocket-Hook und Reconnection

Claude Code generiert typsichere React-Hooks für Socket.io — mit korrektem useEffect cleanup, Reconnection-Handling und optimiertem Re-Render-Verhalten.

useSocket-Hook

// hooks/useSocket.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';

interface UseSocketOptions {
  url: string;
  namespace?: string;
  token?: string;
  autoConnect?: boolean;
}

export function useSocket({ url, namespace = '', token, autoConnect = true }: UseSocketOptions) {
  const socketRef = useRef<Socket | null>(null);
  const [connected, setConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const socket = io(`${url}${namespace}`, {
      auth: { token },
      transports: ['websocket', 'polling'],
      autoConnect,
      reconnection: true,
      reconnectionAttempts: 5,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 10000,
    });

    socket.on('connect', () => { setConnected(true); setError(null); });
    socket.on('disconnect', () => setConnected(false));
    socket.on('connect_error', (err) => setError(err.message));

    socketRef.current = socket;

    // Cleanup: Socket beim Unmount disconnecten
    return () => {
      socket.disconnect();
      socket.removeAllListeners();
      socketRef.current = null;
    };
  }, [url, namespace, token]); // Nur neu verbinden wenn sich URL/Token ändert

  const emit = useCallback(async <T>(event: string, data?: unknown): Promise<T> => {
    if (!socketRef.current?.connected) throw new Error('Not connected');
    return socketRef.current.timeout(5000).emitWithAck(event, data);
  }, []);

  return { socket: socketRef.current, connected, error, emit };
}

Chat-Component mit useSocket

// components/ChatRoom.tsx
export function ChatRoom({ room }: { room: string }) {
  const { socket, connected, emit } = useSocket({
    url: process.env.NEXT_PUBLIC_SOCKET_URL!,
    namespace: '/chat',
    token: useAuthToken(), // JWT aus Auth-Context
  });
  const [messages, setMessages] = useState<Message[]>([]);
  const [text, setText] = useState('');

  useEffect(() => {
    if (!socket) return;

    // Room joinen wenn connected
    socket.emit('joinRoom', room, (ack) => {
      if (!ack.success) console.error('Join failed:', ack.error);
    });

    const onMessage = (msg: Message) => {
      setMessages((prev) => [...prev, msg]);
    };

    socket.on('message', onMessage);
    return () => { socket.off('message', onMessage); }; // Cleanup!
  }, [socket, room]);

  const handleSend = async () => {
    if (!text.trim()) return;
    try {
      await emit('sendMessage', { room, text });
      setText('');
    } catch {
      alert('Senden fehlgeschlagen — bitte erneut versuchen');
    }
  };

  return (
    <div className="chat-container">
      <div className="status">{connected ? '🟢 Verbunden' : '🔴 Getrennt'}</div>
      <div className="messages">
        {messages.map((m) => <div key={m.id}>{m.text}</div>)}
      </div>
      <input value={text} onChange={(e) => setText(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && handleSend()} />
      <button onClick={handleSend} disabled={!connected}>Senden</button>
    </div>
  );
}
useEffect Cleanup ist Pflicht

Ohne socket.off('message', onMessage) im Cleanup registriert jedes Re-Render einen neuen Listener — das führt zu doppelten Nachrichten und Memory Leaks. Claude Code generiert immer korrekte Cleanup-Funktionen.

Reconnection-State-Management

// Reconnection-Events abfangen
useEffect(() => {
  if (!socket) return;

  socket.on('reconnect', (attempt) => {
    console.log(`Reconnected after ${attempt} attempts`);
    // Rooms erneut joinen nach Reconnect
    socket.emit('joinRoom', room, () => {});
  });

  socket.on('reconnect_failed', () => {
    setError('Verbindung konnte nicht wiederhergestellt werden');
  });

  return () => {
    socket.off('reconnect');
    socket.off('reconnect_failed');
  };
}, [socket, room]);

Fazit: Socket.io mit Claude Code — Production-ready in Minuten

Socket.io + Claude Code ist ein starkes Duo für Echtzeit-Features. Die wichtigsten Punkte auf einen Blick:

Echtzeit-Apps mit KI-Support bauen?

Claude Code generiert typsichere Socket.io-Architekturen, Auth-Middleware und React-Hooks — starte deinen kostenlosen Trial und baue deine erste Echtzeit-App heute.

Jetzt kostenlos starten →