A reconnecting WebSocket wrapper for TypeScript/JavaScript. Exponential backoff with jitter, heartbeat ping/pong, a bounded message queue for while you're disconnected, and a small event-emitter API — zero runtime dependencies.
It works with any W3C-compatible WebSocket constructor: the browser's
native WebSocket, or Node's ws
package. In Node you must bring your own WebSocket implementation —
this library does not bundle one, on purpose (see Design notes).
npm install @ferrow/websocket-client
# Node also needs a WebSocket implementation, e.g.:

npm install wsBrowser:
import { ReconnectingWebSocket } from "websocket-client";
const client = new ReconnectingWebSocket("wss://example.com/socket", {
WebSocket, // the browser's native global
});
client.on("open", () => console.log("connected"));
client.on("message", (ev) => console.log("received:", ev.data));
client.on("reconnect", () => console.log("reconnected after a drop"));
client.on("close", () => console.log("disconnected"));
client.send("hello"); // queued automatically if not yet openNode (using ws):
import { ReconnectingWebSocket } from "websocket-client";
import WebSocket from "ws";
const client = new ReconnectingWebSocket("wss://example.com/socket", {
WebSocket: WebSocket as any,
heartbeatInterval: 30_000,
});| Option | Default | Description |
|---|---|---|
WebSocket |
required | A WebSocket constructor. |
protocols |
undefined |
Passed through to the constructor. |
minReconnectDelay |
1000 |
Base backoff delay (ms). |
maxReconnectDelay |
30000 |
Backoff ceiling (ms). |
reconnectDecay |
2 |
Backoff multiplier per attempt. |
jitter |
0.5 |
Jitter fraction (0–1) randomized into each delay. |
maxReconnectAttempts |
Infinity |
Stop retrying after this many attempts. |
heartbeatInterval |
0 (off) |
Interval (ms) between app-level pings. |
heartbeatTimeout |
5000 |
Force-close if no pong arrives within this window. |
heartbeatMessage |
"ping" |
Payload sent as the heartbeat. |
heartbeatPongMessage |
"pong" |
Payload expected back; matching messages are swallowed, not emitted as message. |
maxQueueSize |
100 |
Oldest queued messages are dropped past this. |
autoConnect |
true |
Connect immediately on construction. |
connect()— (re)establish the connection.send(data)— send now if open, otherwise queue (FIFO, bounded bymaxQueueSize).close(code?, reason?)— close permanently; disables auto-reconnect.on(event, listener)/off(event, listener)readyState— the underlying socket'sreadyState, or-1before first connect.queuedMessageCount— messages currently queued.
open, message, close, reconnect (fires alongside open specifically after a drop), error.
The library takes the WebSocket constructor as an injected option instead
of depending on ws directly. That keeps it zero runtime dependencies
and framework-agnostic: it works unmodified in a browser, and in Node it
works with whatever WS client you already have installed (ws,
isomorphic-ws, a mock for tests) without version-pinning someone else's
package for them. The heartbeat is application-level (send/expect a
message) because the WebSocket protocol's native ping/pong frames aren't
exposed consistently across environments — if your server already speaks
protocol-level ping/pong, you can leave heartbeatInterval at 0 and let
the transport handle it.
Sponsored by Ferrow
Part of the ferrow-toolkit collection · Sponsored by Ferrow