Peer
When a new client connects to the server, crossws creates a peer instance that allows getting information from clients and sending messages to them.
Instance properties
peer.id
Unique random identifier (uuid v4) for the peer.
peer.request?
Access to the upgrade request info. You can use it to do authentication and access users headers and cookies.
peer.remoteAddress?
The IP address of the client.
peer.websocket
Direct access to the WebSocket instance.
peer.websocket, a lightweight proxy increases stability. Refer to the compatibility table for more info.peer.context
The context is an object that contains arbitrary information about the request.
You can augment the PeerContext interface types to add your properties.
declare module "crossws" {
interface PeerContext {
customData?: string[];
}
}
peer.topics
All topics, this peer has been subscribed to.
peer.namespace
Peer's pubsub namespace.
peer.bufferedAmount
Number of bytes queued for transmission but not yet flushed to the client.
Use this to apply backpressure: pause sending while it grows past a high watermark and resume once it drops. This prevents a fast producer (for example a streaming async generator) from filling the adapter's send buffer faster than the client can drain it. The easiest way to wait is peer.waitForDrain().
0. Refer to the compatibility table for more info.Instance methods
peer.send(message, { compress? })
Send a message to the connected client.
peer.waitForDrain({ threshold?, pollInterval?, signal? })
Returns a promise that resolves once peer.bufferedAmount drops to or below threshold bytes (default 0), letting you await backpressure relief in a send loop:
// Throttle a fast producer to the client's pace
for (const chunk of stream) {
peer.send(chunk);
if (peer.bufferedAmount > 1024 * 1024 /* 1MB */) {
await peer.waitForDrain({ threshold: 256 * 1024 /* 256KB */ });
}
}
It resolves immediately when there is no backpressure (or on adapters that do not report bufferedAmount). Otherwise it polls every pollInterval ms (default 100) until the buffer drains, and also resolves early if the connection is no longer open so a send loop never hangs on a dropped client. Pass a signal (e.g. AbortSignal.timeout(ms)) to abort the wait — the promise then rejects with the signal's reason.
awaiting in a loop), use the drain hook together with peer.bufferedAmount.peer.subscribe(topic)
Join a broadcast topic.
peer.unsubscribe(topic)
Leave a broadcast topic.
peer.publish(topic, message)
Broadcast a message to the topic. The peer that the publish is called on itself is always excluded from the broadcast.
peer.close(code?, number?)
Gracefully closes the connection.
Here is a list of close codes:
1000means "normal closure" (default)1009means a message was too big and was rejected1011means the server encountered an error1012means the server is restarting1013means the server is too busy or the client is rate-limited4000through4999are reserved for applications (you can use it!)
To close the connection abruptly, use peer.terminate().
peer.terminate()
Abruptly close the connection.
To gracefully close the connection, use peer.close().
peer.ping(data?)
Send an application-level WebSocket ping control frame to the client.
Pair with the pong hook — embedding a correlatable payload (e.g. a timestamp) in data — to measure round-trip latency:
import { defineHooks } from "crossws";
const hooks = defineHooks({
message(peer, message) {
if (message.text() === "measure-latency") {
// Embed the send time in the ping payload; the client echoes it back
// in the pong so it can be told apart from keepalive pongs.
peer.ping(`rtt:${Date.now()}`);
}
},
pong(peer, data) {
const payload = new TextDecoder().decode(data);
if (!payload.startsWith("rtt:")) return; // keepalive or unrelated pong
const rtt = Date.now() - Number(payload.slice(4));
console.log(`[ws] round-trip latency: ${rtt}ms`);
},
});
Use the ping hook to observe pings the client sends unprompted (e.g. a graphql-ws style client heartbeat).
pong hook also fires for the pongs the client sends in reply to the adapter's own internal keepalive pings (see idleTimeout), not only your peer.ping() calls. To measure RTT reliably, embed a correlatable payload in peer.ping(data) and ignore pongs that don't carry it (as above) rather than relying on a shared timestamp that any pong would consume.ping/pong hooks. Refer to the compatibility table.Compatibility
| Bun | Cloudflare | Cloudflare (durable) | Deno | Node (ws) | Node (μWebSockets) | SSE | |
|---|---|---|---|---|---|---|---|
send() | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
publish() / subscribe() | ✓ | ⨉ | ✓ 1 | ✓ 1 | ✓ 1 | ✓ | ✓ 1 |
close() | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
terminate() | ✓ | ✓ 2 | ✓ | ✓ | ✓ | ✓ | ✓ 2 |
bufferedAmount | ✓ | ⨉ 3 | ⨉ 3 | ✓ | ✓ | ✓ | ⨉ 3 |
drain hook | ✓ | ⨉ 4 | ⨉ 4 | ⨉ 4 | ✓ | ✓ | ⨉ 4 |
ping() | ✓ | ⨉ 5 | ⨉ 5 | ⨉ 5 | ✓ | ✓ | ⨉ 5 |
ping / pong hooks | ✓ | ⨉ 5 | ⨉ 5 | ⨉ 5 | ✓ | ✓ | ⨉ 5 |
request | ✓ | ✓ | ✓ 6 | ✓ | ✓ 7 | ✓ 7 | ✓ |
remoteAddress | ✓ | ⨉ | ⨉ | ✓ | ✓ | ✓ | ⨉ |
websocket.url | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
websocket.extensions | ✓ 8 | ⨉ | ⨉ | ✓ 8 | ✓ 8 | ✓ 8 | ⨉ |
websocket.protocol | ✓ 9 | ✓ 9 | ✓ 9 | 9 ✓ | ✓ 9 | ✓ 9 | ⨉ |
websocket.readyState | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ 10 | ✓ 10 |
websocket.binaryType | ✓ 11 | ⨉ | ⨉ | ✓ | ✓ 11 | ✓ | ⨉ |
websocket.bufferedAmount | ⨉ | ⨉ | ⨉ | ✓ | ✓ | ✓ | ⨉ |
Footnotes
- pubsub is not natively handled by runtime. peers are internally tracked. ↩ ↩2 ↩3 ↩4
close()will be used for compatibility. ↩ ↩2- The runtime exposes no send-buffer signal, so
peer.bufferedAmountreports0. (Cloudflare buffers and applies backpressure internally; SSE has no equivalent.) ↩ ↩2 ↩3 - The runtime emits no drain signal. Poll
peer.bufferedAmountinstead where it is available. ↩ ↩2 ↩3 ↩4 - The runtime does not expose a ping/pong control-frame API to user code (pings/pongs are still handled transparently at the protocol level for liveness). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
- After durable object's hibernation, only
request.url(andpeer.id) remain available due to 2048 byte in-memory state limit. ↩ - using a proxy for Request compatible interface (
url,headersonly) wrapping Node.js requests. ↩ ↩2 websocket.extensionsis polyfilled usingsec-websocket-extensionsrequest header. ↩ ↩2 ↩3 ↩4websocket.protocolis polyfilled usingsec-websocket-protocolrequest header. ↩ ↩2 ↩3 ↩4 ↩5 ↩6websocket.readyStateis polyfilled by tracking open/close events. ↩ ↩2- Some runtimes have non standard values including
"nodebuffer"and"uint8array". crossws auto converts them formessage.data. ↩ ↩2