Skip to content

@insession/ws-resilient-transport

A tiny (~130 LOC), dependency-free WebSocket transport that reconnects the way a production deployment actually needs it to.

Most reconnecting-WebSocket libraries give you generic backoff. The thing that bites you in production is different: on every deploy, every client is dropped at the same instant. You want those clients back immediately (the new server instance is already up behind a healthcheck), but you do not want thousands of them reconnecting on the same millisecond and stampeding it. And when the server closes a connection for good, you want reconnection to actually stop.

This transport handles exactly that:

  • Fast reconnect on service restart — a configurable close code (RFC 6455 1012 Service Restart is the conventional choice) reconnects after a short fixed delay instead of waiting out a backoff.
  • Jittered exponential backoff on every other drop, capped at a maximum. Every wait carries ±jitterRatio of randomness so a fleet cut at once spreads out (no thundering herd).
  • Terminal close codes — a set of codes on which the transport stops reconnecting entirely (the server has said it will never accept this connection again).
  • Resume signal — the first reconnect after a service restart tells your handshake resumedFromServiceRestart: true, so the server can suppress re-join side effects (re-broadcasting presence, etc.).

Generic over message types, JSON by default, and fully injectable (WebSocket implementation, timers, RNG) for Node and deterministic tests.

Terminal window
npm install @insession/ws-resilient-transport

Published as a built ESM package (dist/index.js + dist/index.d.ts), no runtime dependencies.

import { createResilientWebSocket } from '@insession/ws-resilient-transport';
type ClientMsg = { type: string; [k: string]: unknown };
type ServerMsg = { type: string; [k: string]: unknown };
let alive = true;
const transport = createResilientWebSocket<ClientMsg, ServerMsg>({
url: 'wss://example.com/ws',
// Sent as soon as each connection opens (auth / join handshake).
buildOpenMessage: async ({ resumedFromServiceRestart }) => {
const token = await getIdToken();
return { type: 'join', token, resume: resumedFromServiceRestart };
},
onMessage: (msg) => handle(msg),
onReconnecting: () => showStatus('reconnecting…'),
isActive: () => alive, // return false on teardown to stop everything
// Deploy semantics: RFC 6455 1012 = fast reconnect; 4001 = terminal.
serviceRestartCode: 1012,
terminalCloseCodes: [4001],
});
transport.connect();
transport.send({ type: 'chat', text: 'hi' });
// On teardown:
alive = false;
transport.close();

Nothing to install — this is just a close-code convention. On graceful shutdown, close each socket with your serviceRestartCode so clients take the fast path:

for (const ws of sockets) ws.close(1012, 'server-restart');

Pair it with a healthcheck that only reports ready once the new instance can accept connections, so the fast reconnect lands on a live server.

createResilientWebSocket<TSend, TRecv>(options){ connect, send, close, socket }.

OptionDefaultMeaning
urlEndpoint to connect to.
onMessage(msg)Called with every parsed inbound message.
buildOpenMessage(ctx)Build the first message on open. ctx.resumedFromServiceRestart is true only on the fast reconnect after a service restart. Return null/throw to send nothing.
onReconnecting()Called right before a reconnect is scheduled.
isActive()() => trueGate checked before delivering messages and before reconnecting.
reconnectDelay500Base backoff (ms) for the first normal reconnect.
maxReconnectDelay15000Backoff cap (ms).
serviceRestartCodenullClose code that means “come straight back”. null disables the fast path.
serviceRestartDelay250Fast-path delay (ms).
terminalCloseCodes[]Close codes on which to stop reconnecting.
jitterRatio0.3±fraction of jitter on every wait.
serialize / deserializeJSON.stringify / JSON.parseWire codec.
WebSocketglobalThis.WebSocketImplementation to use (e.g. ws in Node).
timersglobal set/clearTimeoutInjectable for tests.
randomMath.randomInjectable RNG for deterministic tests.

Backoff for attempt n (1-indexed) is min(reconnectDelay · 2^(n−1), maxReconnectDelay), then jittered — except the first reconnect after serviceRestartCode, which uses serviceRestartDelay.

Terminal window
node --test

The tests use a fake WebSocket plus injected timers and RNG, so they are fully deterministic (no real sockets, no wall-clock waits).

Extracted from InSession’s realtime sync layer, where it keeps synchronized watch-party sessions alive across deploys. Generalizing it meant replacing the product’s protocol types with generics and turning the hard-coded close codes into configuration.

MIT