Use Cases (Updated: 6/1/2026)

Claude Code WebSocket Realtime Implementation: Auth, Reconnects, Heartbeats, and Rooms

Build reliable Claude Code WebSocket realtime with auth, reconnects, heartbeat, rooms, validation, and review.

Claude Code WebSocket Realtime Implementation: Auth, Reconnects, Heartbeats, and Rooms

WebSocket is useful when a screen must change the moment the server knows something: a Claude Code job changes phase, a reviewer leaves a comment, a teammate joins a room, or an operations alert lands. Claude Code can generate a first draft quickly, but a production-ready realtime feature needs more than a working onmessage handler.

The parts that usually break are authentication, reconnect storms, dead mobile connections, malformed JSON, slow consumers, room leakage, and missing audit logs. This article gives you a copy-pasteable Node.js ws implementation plus a browser client, schema validator, heartbeat pattern, test client, and a Claude review prompt.

For the platform basics, keep the official references nearby: MDN WebSocket, readyState, and close. For Claude Code workflow discipline, see Claude Code common workflows.

When WebSocket Is Appropriate

WebSocket is a long-lived, bidirectional channel. It does not replace HTTP APIs, durable queues, database writes, billing flows, or audit logs. Use it for fast coordination; keep durable business actions on reliable server-side paths.

OptionBest forAvoid forClaude Code instruction
WebSocketChat, collaborative UI, live job progress, room broadcastsRare notifications, searchable historyInclude auth, reconnects, heartbeat, and backpressure checks
Server-Sent EventsOne-way server-to-browser updatesFrequent browser-to-server actionsDesign event IDs and event types
PollingAdmin pages that refresh every 15-60 secondsLow-latency UXAdd caching and rate limits
HTTP APISaving, billing, audit, admin changesTyping indicators or high-frequency presenceReuse the same authorization checks

Practical use cases include live Claude Code progress for long-running tasks, review rooms where comments and read states update instantly, operations dashboards that broadcast alerts by customer or team, and training sessions where an instructor sees each participant’s exercise status in realtime.

Architecture

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 means the receiver cannot keep up and the sender’s buffer grows. MDN notes that the classic WebSocket API does not apply backpressure automatically, so the implementation must watch bufferedAmount and close or slow down risky connections.

Prompt Claude Code With Failure Modes

Start with constraints, not just the feature name.

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

Runnable Node ws Server

Run it locally:

npm init -y
npm install ws
node server.mjs

Create server.mjs. In production, replace DEV_TOKEN with a short-lived ticket, session cookie, or identity already verified by your gateway.

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 With Reconnect and Backoff

Browsers cannot attach arbitrary Authorization headers to the WebSocket constructor. For browser apps, issue a short-lived ticket over HTTPS and send it in the first auth message over wss. Do not put secrets in query strings because proxies, logs, and analytics tools can capture them.

<!doctype html>
<html lang="en">
  <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>

Review the Message Schema Separately

Ask Claude Code to review the schema before it reviews the whole server. A vague schema is where large payloads, unknown types, and room spoofing usually sneak in.

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 and Slow Consumers

Heartbeat checks whether a connection is still alive. In ws, the server sends ping; browsers answer protocol-level pong automatically.

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

A slow consumer is connected but cannot process incoming data fast enough. Before every send, check bufferedAmount; if it is too high, close with 1013 and let the client reconnect later.

Test Client

Use this as a second participant without opening another 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());
});

Run node test-client.mjs, stop the server, restart it, and confirm that the browser reconnects with backoff.

Common Pitfalls

  • Putting secrets in ws://example.com?token=..., which leaks into logs and screenshots.
  • Calling JSON.parse without validating type, size, room, and text length.
  • Sending without checking readyState.
  • Skipping heartbeat and keeping dead mobile connections forever.
  • Treating WebSocket broadcasts as audit logs. Important events still need durable storage.
  • Asking Claude Code to “make chat” without specifying auth, reconnects, backpressure, close codes, and tests.

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

For adjacent implementation discipline, read Claude Code security best practices, code review checklist, and webhook implementation.

Training, Templates, and Consultation

The sample above is enough for a local proof of concept. For a team rollout, you also need CLAUDE.md, review rules, close-code policy, audit storage, and rollback handling. ClaudeCodeLab offers Claude Code training and consultation and practical templates and product guides for teams that want to make realtime automation part of their actual workflow.

Conclusion

WebSocket is a strong fit for Claude Code progress streams, review rooms, collaborative tools, and operational alerts. It is not safe just because it connects. Treat auth handshake, room routing, validation, reconnects, heartbeat, backpressure, and audit logging as one design.

After trying the implementation in this article locally, chat before auth was rejected, browser reconnects worked after a server restart, and room broadcasts stayed scoped. The part that still needed team-specific tuning was operational policy: buffer thresholds, close-code wording, and how audit events should be stored. Masa’s practical takeaway is that Claude Code is most valuable when it is asked to review the failure modes, not only generate the happy path.

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

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.