Use Cases (Actualizado: 1/6/2026)

WebSocket en tiempo real con Claude Code: autenticacion, reconexion, heartbeat y rooms

Implementa WebSocket fiable con Claude Code: autenticacion, reconexion, heartbeat, rooms, validacion y revision segura.

WebSocket en tiempo real con Claude Code: autenticacion, reconexion, heartbeat y rooms

WebSocket tiene sentido cuando la pantalla debe cambiar justo cuando el servidor sabe algo nuevo: un trabajo de Claude Code pasa a pruebas, aparece un comentario en una revision, un usuario entra en una sala o llega una alerta operativa. Claude Code puede generar un borrador rapido, pero una funcion realtime lista para produccion necesita mas que un onmessage que funcione.

Los fallos habituales estan en la autenticacion, las tormentas de reconexion, las conexiones moviles muertas, el JSON invalido, los consumidores lentos, las fugas entre rooms y la falta de logs de auditoria. Esta guia incluye un servidor Node.js con ws, un cliente de navegador con backoff, validador de mensajes, heartbeat, cliente de prueba y prompt de revision para Claude Code.

Para la base de la API, revisa MDN WebSocket, readyState y close. Para trabajar con Claude Code con mas disciplina, consulta Claude Code common workflows.

Cuando conviene usar WebSocket

WebSocket es un canal bidireccional de larga duracion. No sustituye a HTTP API, colas, escrituras en base de datos, cobros ni logs de auditoria. Sirve para coordinacion rapida; las acciones importantes deben seguir un camino durable del lado servidor.

OpcionMejor paraEvitar enInstruccion para Claude Code
WebSocketChat, UI colaborativa, progreso en vivo, broadcast por roomNotificaciones poco frecuentes, historiales buscablesIncluir auth, reconexion, heartbeat y backpressure
Server-Sent EventsEventos de servidor a navegadorAcciones frecuentes del navegador al servidorDisenar IDs y tipos de evento
PollingPaneles admin que refrescan cada 15-60 segundosUX de baja latenciaAgregar cache y rate limit
HTTP APIGuardado, pagos, auditoria, cambios adminTyping indicators o presencia frecuenteReutilizar la misma autorizacion

Tres usos practicos: mostrar el progreso de tareas largas de Claude Code, sincronizar comentarios y estados de lectura en una sala de revision, y emitir alertas operativas por cliente o equipo. En formacion, tambien sirve para que el instructor vea el estado de ejercicios de cada participante en tiempo real.

Arquitectura

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 significa que el receptor no procesa lo bastante rapido y el buffer del emisor crece. MDN advierte que la API WebSocket clasica no aplica backpressure automaticamente, asi que hay que vigilar bufferedAmount y cerrar o limitar conexiones peligrosas.

Pide a Claude Code que piense en fallos

No pidas solo “un chat realtime”. Define las condiciones de seguridad desde el primer prompt.

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

Servidor Node ws ejecutable

Ejecucion local:

npm init -y
npm install ws
node server.mjs

Crea server.mjs. En produccion, reemplaza DEV_TOKEN por un ticket de vida corta, una cookie de sesion o una identidad verificada por tu 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}`);
});

Cliente de navegador con reconexion y backoff

El constructor WebSocket del navegador no permite headers Authorization arbitrarios. En una aplicacion real, pide un ticket de vida corta por HTTPS y mandalo como primer mensaje auth sobre wss. No pongas secretos en query strings.

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

Revisa el schema de mensajes por separado

Pide a Claude Code revisar el schema antes que todo el servidor. Si el contrato es ambiguo, se cuelan payloads grandes, tipos desconocidos y roomId falsificados.

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 y consumidores lentos

Heartbeat comprueba si la conexion sigue viva. Con ws, el servidor manda ping y el navegador responde pong a nivel de protocolo.

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

Un consumidor lento sigue conectado, pero no procesa mensajes a tiempo. Antes de cada envio, revisa bufferedAmount; si es demasiado alto, cierra con 1013 y deja que el cliente reintente despues.

Cliente de prueba

Usalo como segundo participante sin abrir otro navegador.

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());
});

Ejecuta node test-client.mjs, reinicia el servidor y confirma que el navegador se reconecta con backoff.

Errores frecuentes

  • Poner secretos en ws://example.com?token=..., filtrandolos a logs y capturas.
  • Hacer solo JSON.parse sin validar tipo, tamano, room y longitud de texto.
  • Enviar sin comprobar readyState.
  • Omitir heartbeat y conservar conexiones muertas.
  • Usar broadcasts WebSocket como si fueran logs de auditoria. Los eventos importantes necesitan almacenamiento durable.
  • Pedir a Claude Code “haz un chat” sin exigir auth, reconexion, backpressure, close codes y pruebas.

Prompt de revision para Claude Code

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

Para temas relacionados, lee buenas practicas de seguridad con Claude Code, checklist de revision de codigo y guia de webhooks.

Formacion, templates y consultoria

El ejemplo basta para un PoC local. En un equipo tambien necesitas CLAUDE.md, reglas de revision, politica de close codes, almacenamiento de auditoria y rollback. ClaudeCodeLab ofrece training and consultation y templates and product guides para convertir la automatizacion realtime en una practica estable.

Conclusion

WebSocket encaja bien con progreso de Claude Code, salas de revision, herramientas colaborativas y alertas operativas. Pero estar conectado no significa estar seguro. Trata autenticacion, room routing, validacion, reconexion, heartbeat, backpressure y auditoria como un unico diseno.

Al probar localmente la implementacion de este articulo, chat antes de auth fue rechazado, el navegador se reconecto tras reiniciar el servidor y los broadcasts quedaron dentro del room correcto. Lo que aun requiere ajuste por equipo son los umbrales de buffer, los textos de close code y el destino de auditoria. La conclusion practica de Masa: Claude Code aporta mucho mas cuando revisa modos de fallo, no solo cuando genera el camino feliz.

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

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.