Use Cases (Aktualisiert: 1.6.2026)

Claude Code WebSocket Realtime: Auth, Reconnect, Heartbeat und Rooms

WebSocket mit Claude Code robust umsetzen: Auth, Reconnect, Heartbeat, Rooms, Validierung, Testclient und Review.

Claude Code WebSocket Realtime: Auth, Reconnect, Heartbeat und Rooms

WebSocket passt, wenn eine Oberflaeche sofort reagieren muss, sobald der Server etwas Neues weiss: Ein Claude Code Job wechselt in die Testphase, ein Review-Kommentar erscheint, ein Nutzer betritt einen Raum, oder ein Betriebsalarm soll direkt sichtbar sein. Claude Code kann schnell einen ersten Entwurf erzeugen, aber produktionsreife Realtime-Funktionen brauchen mehr als einen funktionierenden onmessage Handler.

Typische Bruchstellen sind Authentifizierung, Reconnect-Stuerme, tote Mobilverbindungen, ungueltiges JSON, langsame Empfaenger, Raum-Leaks und fehlende Audit-Logs. Dieser Artikel liefert einen kopierbaren Node.js Server mit ws, einen Browser-Client mit Backoff, Message-Schema-Validierung, Heartbeat, Testclient und einen Claude Code Review-Prompt.

Die offiziellen Grundlagen stehen in MDN WebSocket, readyState und close. Fuer den Arbeitsprozess mit Claude Code ist Claude Code common workflows hilfreich.

Wann WebSocket sinnvoll ist

WebSocket ist ein langlebiger bidirektionaler Kanal. Er ersetzt keine HTTP APIs, Queues, Datenbankwrites, Zahlungsablaeufe oder Audit-Logs. Nutze ihn fuer schnelle Koordination; dauerhafte Geschaeftsereignisse bleiben auf verlaesslichen Serverpfaden.

OptionGeeignet fuerEher vermeiden fuerAnweisung an Claude Code
WebSocketChat, kollaborative UI, Live-Fortschritt, Room-BroadcastSeltene Benachrichtigungen, durchsuchbare HistorieAuth, Reconnect, Heartbeat und Backpressure einbauen
Server-Sent EventsEinweg-Events vom Server zum BrowserHaeufige Aktionen vom Browser zum ServerEvent-IDs und Typen festlegen
PollingAdmin-Seiten mit Refresh alle 15-60 SekundenLow-Latency UXCache und Rate Limit ergaenzen
HTTP APISpeichern, Zahlung, Audit, Admin-AenderungenTyping-Status oder PresenceDieselben Authz-Checks wiederverwenden

Praktische Faelle: Live-Fortschritt langer Claude Code Aufgaben, Review-Rooms mit sofortigen Kommentaren und Read-States, sowie Operations-Dashboards mit Alerts pro Kunde oder Team. In Trainings kann der Trainer den Uebungsstatus aller Teilnehmenden live sehen.

Architektur

flowchart LR
  Browser["Browser client"] -->|"auth message + join room"| Server["Node ws server"]
  Server --> Validator["JSON schema validator"]
  Server --> Rooms["room registry"]
  Server --> Audit["audit log / database"]
  Rooms -->|"broadcast"| Browser
  Server -->|"ping"| Browser
  Browser -->|"auto pong / reconnect"| Server
  Claude["Claude Code review loop"] --> Server

Backpressure bedeutet, dass der Empfaenger nicht schnell genug verarbeitet und der Sendepuffer waechst. MDN weist darauf hin, dass die klassische WebSocket API Backpressure nicht automatisch anwendet. Deshalb muss die Implementierung bufferedAmount beobachten.

Fehlerfaelle in den Prompt schreiben

Bitte Claude Code nicht nur um “einen Chat”. Gib die Sicherheits- und Betriebsbedingungen direkt mit.

Create a Node.js ws realtime WebSocket foundation.
Requirements:
- Browser sends an auth message after connect; join/chat are forbidden before auth
- Clients join by roomId and broadcasts only go to the same room
- Validate JSON messages by type and close invalid messages with 1008
- Implement heartbeat with server ping and pong tracking
- Close slow consumers with 1013 when bufferedAmount is too high
- Browser client uses exponential backoff with jitter
- Do not put secrets in URL query strings
- Do not treat WebSocket delivery as an audit log replacement

Ausfuehrbarer Node ws Server

Lokal starten:

npm init -y
npm install ws
node server.mjs

Lege server.mjs an. In Produktion ersetzt du DEV_TOKEN durch ein kurzlebiges Ticket, Session-Cookie oder eine vom Gateway gepruefte Identitaet.

import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { WebSocket, WebSocketServer } from "ws";

const PORT = Number(process.env.PORT || 8080);
const DEV_TOKEN = process.env.DEV_TOKEN || "dev-token";
const AUTH_TIMEOUT_MS = 5000;
const HEARTBEAT_INTERVAL_MS = 30000;
const MAX_BUFFERED_BYTES = 1024 * 1024;

const rooms = new Map();
const server = createServer((req, res) => {
  res.writeHead(200, { "content-type": "text/plain" });
  res.end("WebSocket server is running\n");
});
const wss = new WebSocketServer({ server });

function isRoomId(value) {
  return typeof value === "string" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value);
}

function validateClientMessage(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return { ok: false, error: "message must be an object" };
  }
  if (typeof value.type !== "string") {
    return { ok: false, error: "type is required" };
  }

  if (value.type === "auth") {
    if (typeof value.token !== "string" || value.token.length < 8) {
      return { ok: false, error: "auth.token is invalid" };
    }
    return { ok: true, value: { type: "auth", token: value.token } };
  }

  if (value.type === "join") {
    if (!isRoomId(value.roomId)) return { ok: false, error: "roomId is invalid" };
    return { ok: true, value: { type: "join", roomId: value.roomId } };
  }

  if (value.type === "chat") {
    if (!isRoomId(value.roomId)) return { ok: false, error: "roomId is invalid" };
    if (typeof value.text !== "string" || value.text.length < 1 || value.text.length > 1000) {
      return { ok: false, error: "text must be 1-1000 chars" };
    }
    return {
      ok: true,
      value: {
        type: "chat",
        roomId: value.roomId,
        requestId: typeof value.requestId === "string" ? value.requestId.slice(0, 100) : randomUUID(),
        text: value.text,
      },
    };
  }

  if (value.type === "ping") return { ok: true, value: { type: "ping" } };
  return { ok: false, error: `unsupported type: ${value.type}` };
}

function decodeMessage(raw) {
  if (raw.length > 8192) return { ok: false, error: "message too large" };
  try {
    return validateClientMessage(JSON.parse(raw.toString("utf8")));
  } catch {
    return { ok: false, error: "invalid JSON" };
  }
}

function sendJson(ws, payload) {
  if (ws.readyState !== WebSocket.OPEN) return;
  if (ws.bufferedAmount > MAX_BUFFERED_BYTES) {
    ws.close(1013, "slow consumer");
    return;
  }
  ws.send(JSON.stringify(payload));
}

function joinRoom(ws, roomId) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);
  ws.rooms.add(roomId);
}

function leaveAllRooms(ws) {
  for (const roomId of ws.rooms) {
    const members = rooms.get(roomId);
    if (!members) continue;
    members.delete(ws);
    if (members.size === 0) rooms.delete(roomId);
  }
  ws.rooms.clear();
}

function broadcast(roomId, payload) {
  const members = rooms.get(roomId);
  if (!members) return;
  for (const client of members) sendJson(client, payload);
}

wss.on("connection", (ws) => {
  ws.id = randomUUID();
  ws.user = null;
  ws.rooms = new Set();
  ws.isAlive = true;

  const authTimer = setTimeout(() => {
    if (!ws.user) ws.close(4401, "auth required");
  }, AUTH_TIMEOUT_MS);

  ws.on("pong", () => {
    ws.isAlive = true;
  });

  ws.on("message", (raw) => {
    const decoded = decodeMessage(raw);
    if (!decoded.ok) {
      ws.close(1008, decoded.error);
      return;
    }

    const msg = decoded.value;
    if (!ws.user) {
      if (msg.type !== "auth" || msg.token !== DEV_TOKEN) {
        ws.close(4403, "auth failed");
        return;
      }
      ws.user = { id: `user-${ws.id.slice(0, 8)}` };
      clearTimeout(authTimer);
      sendJson(ws, { type: "auth_ok", connectionId: ws.id, userId: ws.user.id });
      return;
    }

    if (msg.type === "join") {
      joinRoom(ws, msg.roomId);
      sendJson(ws, { type: "joined", roomId: msg.roomId });
      return;
    }

    if (msg.type === "chat") {
      if (!ws.rooms.has(msg.roomId)) {
        ws.close(1008, "join room before chat");
        return;
      }
      const event = {
        type: "chat",
        id: randomUUID(),
        requestId: msg.requestId,
        roomId: msg.roomId,
        userId: ws.user.id,
        text: msg.text,
        sentAt: new Date().toISOString(),
      };
      console.log(JSON.stringify({ audit: true, event }));
      broadcast(msg.roomId, event);
      return;
    }

    if (msg.type === "ping") sendJson(ws, { type: "pong", sentAt: new Date().toISOString() });
  });

  ws.on("close", () => {
    clearTimeout(authTimer);
    leaveAllRooms(ws);
  });
});

setInterval(() => {
  for (const ws of wss.clients) {
    if (ws.isAlive === false) {
      leaveAllRooms(ws);
      ws.terminate();
      continue;
    }
    ws.isAlive = false;
    ws.ping();
  }
}, HEARTBEAT_INTERVAL_MS).unref();

server.listen(PORT, () => {
  console.log(`WebSocket server listening on ws://localhost:${PORT}`);
});

Browser-Client mit Reconnect und Backoff

Der Browser-Constructor WebSocket erlaubt keine frei gesetzten Authorization Header. In Browser-Apps holst du deshalb per HTTPS ein kurzlebiges Ticket und sendest es als erstes auth Message ueber wss. Secrets gehoeren nicht in Query Strings.

<!doctype html>
<html lang="de">
  <body>
    <form id="form">
      <input id="text" autocomplete="off" placeholder="message" />
      <button>send</button>
    </form>
    <pre id="log"></pre>

    <script>
      const roomId = "room:demo";
      const token = "dev-token";
      const log = document.querySelector("#log");
      const form = document.querySelector("#form");
      const input = document.querySelector("#text");

      let socket;
      let attempt = 0;
      let stopped = false;

      function write(line) {
        log.textContent += `${line}\n`;
      }

      function reconnectDelay() {
        const base = Math.min(1000 * 2 ** attempt, 10000);
        attempt += 1;
        return base + Math.floor(Math.random() * 300);
      }

      function send(payload) {
        if (!socket || socket.readyState !== WebSocket.OPEN) return false;
        if (socket.bufferedAmount > 512 * 1024) {
          write("send skipped: browser buffer is high");
          return false;
        }
        socket.send(JSON.stringify(payload));
        return true;
      }

      function connect() {
        socket = new WebSocket("ws://localhost:8080");
        write("connecting");

        socket.addEventListener("open", () => {
          attempt = 0;
          send({ type: "auth", token });
          send({ type: "join", roomId });
        });

        socket.addEventListener("message", (event) => {
          const msg = JSON.parse(event.data);
          write(`${msg.type}: ${JSON.stringify(msg)}`);
        });

        socket.addEventListener("close", (event) => {
          write(`closed code=${event.code} reason=${event.reason}`);
          if (!stopped) window.setTimeout(connect, reconnectDelay());
        });

        socket.addEventListener("error", () => {
          write("socket error");
        });
      }

      form.addEventListener("submit", (event) => {
        event.preventDefault();
        const text = input.value.trim();
        if (!text) return;
        send({ type: "chat", roomId, requestId: crypto.randomUUID(), text });
        input.value = "";
      });

      window.addEventListener("beforeunload", () => {
        stopped = true;
        if (socket && socket.readyState === WebSocket.OPEN) socket.close(1000, "page unload");
      });

      connect();
    </script>
  </body>
</html>

Message-Schema separat pruefen

Lass Claude Code zuerst das Schema pruefen, dann den ganzen Server. Ungenaue Schemas lassen grosse Payloads, unbekannte Typen und gefaelschte roomId leichter durch.

export function validateClientMessage(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return { ok: false, error: "message must be an object" };
  }
  if (value.type === "join" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value.roomId)) {
    return { ok: true, value: { type: "join", roomId: value.roomId } };
  }
  if (value.type === "chat" && /^[a-zA-Z0-9:_-]{1,64}$/.test(value.roomId)) {
    const text = typeof value.text === "string" ? value.text.trim() : "";
    if (text.length >= 1 && text.length <= 1000) {
      return { ok: true, value: { type: "chat", roomId: value.roomId, text } };
    }
  }
  return { ok: false, error: "invalid realtime message" };
}

Heartbeat und langsame Empfaenger

Heartbeat prueft, ob eine Verbindung noch lebt. Bei ws sendet der Server ping; Browser antworten automatisch mit einem Protokoll-pong.

setInterval(() => {
  for (const ws of wss.clients) {
    if (ws.isAlive === false) {
      ws.terminate();
      continue;
    }
    ws.isAlive = false;
    ws.ping();
  }
}, 30000);

Ein langsamer Empfaenger bleibt verbunden, verarbeitet aber nicht schnell genug. Vor jedem Senden bufferedAmount pruefen; ist der Wert zu hoch, mit 1013 schliessen und spaeter reconnecten lassen.

Testclient

Damit simulierst du einen zweiten Teilnehmer ohne weiteren Browser.

import { WebSocket } from "ws";

const ws = new WebSocket("ws://localhost:8080");
const roomId = "room:demo";

ws.on("open", () => {
  ws.send(JSON.stringify({ type: "auth", token: "dev-token" }));
  ws.send(JSON.stringify({ type: "join", roomId }));
  setInterval(() => {
    ws.send(JSON.stringify({
      type: "chat",
      roomId,
      requestId: crypto.randomUUID(),
      text: `hello ${new Date().toISOString()}`,
    }));
  }, 3000);
});

ws.on("message", (data) => {
  console.log(data.toString());
});

Starte node test-client.mjs, starte den Server neu und pruefe, ob der Browser mit Backoff wieder verbindet.

Typische Fallen

  • Secrets in ws://example.com?token=..., wodurch sie in Logs und Screenshots landen.
  • Nur JSON.parse, aber keine Validierung von Typ, Groesse, room und Textlaenge.
  • Senden ohne readyState zu pruefen.
  • Kein Heartbeat, wodurch tote Mobilverbindungen bleiben.
  • WebSocket-Broadcasts als Audit-Log missverstehen. Wichtige Events brauchen dauerhafte Speicherung.
  • Claude Code nur “baue Chat” sagen und Auth, Reconnect, Backpressure, Close Codes und Tests vergessen.

Claude Code Review-Prompt

Review this WebSocket implementation before release.

Scope:
- server.mjs
- browser client
- message schema validator

Check:
1. Can a client join/chat before auth?
2. Can secrets leak into URLs, logs, or error messages?
3. Can a client spoof roomId and send to another room?
4. Can reconnect/backoff create a connection storm?
5. Does heartbeat remove dead connections?
6. Are slow consumers handled with bufferedAmount?
7. Is there an audit log outside WebSocket delivery?

Return:
- P0/P1/P2 findings first
- Reproduction steps
- Minimal patch
- Missing tests

Weitere Grundlagen: Claude Code security best practices, code review checklist und webhook implementation.

Training, Templates und Beratung

Das Beispiel reicht fuer einen lokalen PoC. Fuer Teams braucht es auch CLAUDE.md, Review-Regeln, Close-Code-Policy, Audit-Speicherung und Rollback. ClaudeCodeLab bietet training and consultation sowie templates and product guides fuer Teams, die Realtime-Automation sauber in ihre Arbeitsablaeufe bringen wollen.

Fazit

WebSocket passt gut zu Claude Code Fortschrittsstreams, Review-Rooms, Kollaboration und Betriebsalarmen. Eine offene Verbindung ist aber noch keine sichere Loesung. Auth-Handshake, Room-Routing, Validierung, Reconnect, Heartbeat, Backpressure und Audit-Logging gehoeren in ein gemeinsames Design.

Beim lokalen Test der Implementierung wurde chat vor auth abgelehnt, der Browser verband sich nach einem Server-Neustart wieder, und Broadcasts blieben im richtigen room. Teamabhaengig bleiben Buffer-Schwellen, Close-Code-Texte und Audit-Speicherort. Masas praktische Erkenntnis: Claude Code liefert deutlich mehr Qualitaet, wenn es gezielt Fehlerzustaende pruefen soll, statt nur den Happy Path zu erzeugen.

#Claude Code #WebSocket #real-time #ws #Node.js
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.