Realtime WebSockets and SSE skill for Claude Code
Live connections that scale out and recover on their own. Claude picks SSE or WebSockets from the actual traffic pattern, authenticates the handshake with cookies plus origin checks or short-lived tickets, and adds heartbeats tuned below every proxy idle timeout. Slow clients get disconnected before they exhaust memory, events fan out through a broker so any instance can deliver them, and clients reconnect with jitter and resume from a sequence number.
- name
realtime-websockets - size168 lines · ≈2,000 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds realtime features over WebSockets and Server-Sent Events - choosing WS vs SSE vs polling, handshake authentication and origin checks, message envelopes, heartbeats, backpressure for slow clients, horizontal scaling with Redis or NATS pub/sub, reconnection with backoff and resume, proxy and load balancer timeouts, and draining on deploy. Use when adding live updates, chat, notifications, presence or collaborative features, or when sockets drop, leak memory, or stop working behind a proxy or after scaling out. Try asking
- Add live notifications to our app. Should we use SSE or WebSockets?
- Our sockets disconnect every 60 seconds behind nginx
- Messages only reach users on the same server since we added a second instance
Use it when
- Adding chat, notifications, presence, live dashboards or collaboration
- Connections drop, leak or fail behind a proxy or load balancer
- Scaling a socket server from one instance to many
- Designing reconnect and catch-up behavior for clients
Skip it when
- Events flow between backend services, not to clients — use event-driven-architecture
- The realtime layer is Workers Durable Objects config — start with cloudflare-workers
How to check the result
A message published on one instance reaches a client on another, a dropped client reconnects and receives missed events, and a paused reader is closed while server memory stays flat.
The common mistake it prevents
Skipping server heartbeats, so proxies silently cut idle sockets and dead connections accumulate on the server for hours.
A realtime system is done when connections are authenticated at the handshake, dead peers are detected within a minute, slow consumers cannot exhaust server memory, any server instance can deliver any message, and clients reconnect and catch up without user action or a thundering herd.
Workflow
- [ ] 1. Pick the transport from the traffic pattern
- [ ] 2. Define the message protocol (envelope, types, versioning, sequence numbers)
- [ ] 3. Authenticate the handshake; authorize every subscription
- [ ] 4. Heartbeats and idle timeouts across every hop
- [ ] 5. Backpressure and per-connection limits
- [ ] 6. Fan-out across instances (pub/sub) and connection state storage
- [ ] 7. Client reconnection with backoff, jitter and resume
- [ ] 8. Deploy draining, metrics, load test
1. Transport choice
| Pattern | Use |
|---|---|
| Server pushes, client rarely sends (feeds, notifications, progress, LLM token streams) | SSE (text/event-stream): plain HTTP, auto-reconnect, Last-Event-ID resume |
| Bidirectional, low-latency, frequent client messages (chat, games, collaboration, presence) | WebSocket |
| Updates every 30 s+ or very constrained infra | Polling with ETags |
| Browser-to-browser media or data | WebRTC (signaling over WS) |
| Managed option without running sockets | Pusher/Ably/Supabase Realtime/Cloudflare Durable Objects |
SSE over HTTP/1.1 is limited to about 6 connections per origin per browser; serve over HTTP/2 or multiplex streams into one connection. Socket.IO is a protocol on top of WS with its own client; only use it when you want its rooms, acks and fallbacks, and then use its Redis adapter for scaling.
2. Protocol
{ "v": 1, "type": "message.created", "id": "evt_01J...", "seq": 1842, "channel": "room:42", "data": { } }
- Every server event carries a monotonically increasing
seqper channel (or a global event ID) so clients detect gaps and resume. - Client commands carry a client-generated
id; the server replies withackorerrorreferencing it. Commands are idempotent by that ID. - Validate every inbound message against a schema; cap message size (e.g. 64 KB) with the server’s max payload option.
- Version the envelope (
v); old clients live for weeks in open tabs.
3. Authentication and authorization
- Browsers cannot set custom headers on
new WebSocket(url). Options in order of preference:- Same-site session cookie sent with the handshake, plus a strict
Originheader allowlist check on upgrade (prevents cross-site WebSocket hijacking). - Short-lived single-use ticket: client calls
POST /realtime/ticket(authenticated), receives a token valid for 30-60 s, connects withwss://.../ws?ticket=...; server redeems and deletes it. Never put long-lived tokens in URLs (they land in logs). - First message
{type:"auth", token}with a server timeout of a few seconds before closing.
- Same-site session cookie sent with the handshake, plus a strict
- Authorize each subscribe:
subscribe room:42checks membership server-side; channel names from clients are untrusted input. - Re-validate on token expiry: close with code 4001 (app-defined) and let the client reauthenticate, or push a refresh request before expiry.
4. Heartbeats and timeouts
Every hop has an idle timeout that silently kills quiet connections: AWS ALB 60 s default
(configurable), nginx proxy_read_timeout 60 s default, Cloudflare about 100 s, mobile
NATs often less. Send a ping every 20-30 s from the server (WS ping frames, or an SSE
comment line : ping\n\n), and terminate connections that miss two pongs. Server-side
dead-peer detection prevents leaked connections from piling up after network drops.
Proxy configuration for upgrades and buffering is in references/infra-config.md; read it when deploying behind nginx, a cloud load balancer, or Kubernetes ingress.
5. Backpressure
import { WebSocketServer, WebSocket } from "ws";
const wss = new WebSocketServer({ noServer: true, maxPayload: 64 * 1024 });
const MAX_BUFFERED = 1 * 1024 * 1024; // 1 MiB queued per client
function send(ws: WebSocket, msg: object) {
if (ws.readyState !== WebSocket.OPEN) return;
if (ws.bufferedAmount > MAX_BUFFERED) {
ws.close(1013, "slow consumer"); // try again later; client resyncs on reconnect
return;
}
ws.send(JSON.stringify(msg));
}
Policies for slow consumers: drop and resync (above), coalesce (send only the latest state per key), or sample. Never let an unbounded per-connection queue grow. Also cap connections per user and per IP, and inbound message rate per connection.
6. Scaling out
- Each instance holds only its own sockets. Publish events to a broker (Redis pub/sub,
Redis Streams, NATS, Postgres
LISTEN/NOTIFYfor small scale); every instance subscribes and delivers to its local sockets for that channel. - Redis pub/sub is fire-and-forget: an instance that blips misses messages. For
guaranteed catch-up, persist events (Redis Streams with IDs, or a DB table) and let
clients resume from
seq. - Sticky sessions are not required for raw WS/SSE once fan-out uses a broker; they are required for Socket.IO’s HTTP long-polling fallback.
- Presence: store per-connection entries with TTL in Redis (refresh on heartbeat), not in instance memory.
- Capacity: plan memory per connection (tens of KB in Node
wsplus buffers), raise file descriptor limits (ulimit -n), and load test before launch.
7. Client reconnection
let attempt = 0;
async function connect() {
const ws = new WebSocket(`wss://api.example.com/ws?ticket=${await getTicket()}`);
ws.onopen = () => { attempt = 0; ws.send(JSON.stringify({ type: "resume", lastSeq })); };
ws.onclose = (e) => {
if (e.code === 4001) return reauthenticate().then(connect); // app-defined auth failure
const delay = Math.min(30_000, 1000 * 2 ** attempt++) * Math.random(); // full jitter
setTimeout(connect, delay);
};
}
On resume, the server replays events after
lastSeq or tells the client to refetch full state if the gap is too old. Also reconnect
on online and visibilitychange events. SSE’s EventSource reconnects automatically
and sends Last-Event-ID if events carry id: lines; set the retry: field to control delay.
8. Deploys and observability
- On SIGTERM: stop accepting upgrades, send close code 1012 (service restart) or 1001 (going away) to clients in batches spread over 10-30 s, then exit. Clients reconnect to new instances with jitter instead of all at once.
- Metrics: open connections, connects/disconnects per minute by close code, messages in/out per second, send-buffer drops, broker lag, handshake auth failures.
Gotchas
ws://from anhttps://page is blocked as mixed content; alwayswss://in production.- Compression (
permessage-deflate) costs memory per connection; leave it off unless messages are large and repetitive. - Serverless functions cannot hold WebSockets; use API Gateway WebSocket APIs, Durable Objects, or a dedicated service.
- Broadcasting by looping over all sockets on every event does not scale; index sockets by channel.
- Close codes 4000-4999 are for applications; 1006 means abnormal closure (network drop or no close frame), never sent explicitly.
Verification
- Integration test: two clients on different server instances; a message published via instance A arrives at a client on instance B.
- Kill the network (or the server) mid-session: the client reconnects with backoff and receives missed events via resume.
- Simulate a slow consumer (pause reading): server memory stays bounded and the connection closes with 1013.
- Leave a connection idle for 5 minutes behind the real proxy: it stays open.
- Load test (k6
k6/wsorartillery) to target connection count; record memory per connection.
Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
references/infra-config.md
# Proxy and infrastructure configuration for WS and SSE
Read when deploying sockets behind a reverse proxy, cloud load balancer, CDN, or
Kubernetes ingress. Every hop needs: upgrade support (WS), no response buffering (SSE),
and idle timeouts longer than your heartbeat interval.
## nginx
```nginx
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s; # default 60s kills idle sockets
proxy_send_timeout 3600s;
}
location /events { # SSE
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off; # or send header X-Accel-Buffering: no from the app
proxy_cache off;
proxy_read_timeout 3600s;
gzip off; # compression buffers chunks
}
}
```
Reload with `nginx -t && nginx -s reload`. During reload, old workers keep existing
connections until `worker_shutdown_timeout` (unset means they wait forever); set it to a
few minutes so reloads finish.
## AWS Application Load Balancer
- WebSockets work over HTTP/1.1 listeners without extra config.
- Idle timeout: attribute `idle_timeout.timeout_seconds` (default 60, max 4000). Heartbeat
below it.
- Target group deregistration delay (default 300 s) controls how long draining instances
keep connections; align with your SIGTERM drain logic.
- Stickiness is only needed for Socket.IO polling.
## AWS API Gateway WebSocket APIs
- Managed connection handling with `$connect`, `$disconnect`, `$default` routes; push to
clients with the `@connections` management API using the connection ID.
- Idle timeout 10 minutes, max connection duration 2 hours: clients must reconnect.
- Store connection IDs and subscriptions in DynamoDB; delete on `$disconnect` and on
`GoneException` (410) when posting.
## Google Cloud Run
- WebSockets and SSE supported; the request timeout (max 60 minutes) caps connection
life, so clients must reconnect periodically.
- Each socket counts against container concurrency; set `--concurrency` to what one
instance can actually hold and consider session affinity for Socket.IO.
## Cloudflare (proxied)
- WebSockets supported on all plans; idle connections without traffic close after about
100 seconds, so heartbeat every 30 s.
- For SSE, avoid caching the endpoint and do not enable features that buffer responses.
- Durable Objects with the WebSocket Hibernation API keep many idle sockets cheaply.
## Kubernetes ingress-nginx
```yaml
metadata:
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off" # SSE
```
Set `terminationGracePeriodSeconds` above your drain window and add a `preStop` sleep of
a few seconds so endpoints are removed before the process starts closing sockets.
## Node.js `ws` upgrade handling with auth
```ts
server.on("upgrade", async (req, socket, head) => {
const origin = req.headers.origin;
if (!ALLOWED_ORIGINS.has(origin ?? "")) { socket.write("HTTP/1.1 403 Forbidden\r\n\r\n"); return socket.destroy(); }
const user = await authenticateUpgrade(req).catch(() => null); // cookie or ticket
if (!user) { socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n"); return socket.destroy(); }
wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req, user));
});
// heartbeat
const interval = setInterval(() => {
for (const ws of wss.clients) {
if ((ws as any).isAlive === false) { ws.terminate(); continue; }
(ws as any).isAlive = false;
ws.ping();
}
}, 30_000);
wss.on("connection", (ws) => { (ws as any).isAlive = true; ws.on("pong", () => ((ws as any).isAlive = true)); });
wss.on("close", () => clearInterval(interval));
```
## OS limits
- Raise open file limits for the service user (`LimitNOFILE=1048576` in systemd, or the
container runtime's ulimit setting). Each socket is a file descriptor.
- For very high connection counts, tune ephemeral port range and TCP keepalive on proxies
that open upstream connections per client.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.