Deep Dive — WebSocket Sessions
The TypeScript SDK manages WebSocket connections, reconnection, and subscription lifecycles automatically. This guide details session architectures, connection counts, and lifecycle management.
Session architecture
Every WebSocket stream opened via client.websocket.public or client.websocket.private attaches to a WebSocketSession. Sessions manage socket lifecycles, message routing, and subscription replay across three categories:
Shared sessions
Public streams share one underlying WebSocketSession; authenticated streams share a separate authenticated WebSocketSession. This prevents public frames and connection state from crossing the authentication boundary:
public.trades(symbol)— public trade streampublic.bookTicker(symbol)— public best bid/askpublic.depthUpdates(symbol)— public depth diffspublic.contractStatus()— perpetual contract statuspublic.rfqs()— public RFQ feedprivate.orders({ scope })— authenticated order updates (scopeis"account"or"session")private.balances(options)— authenticated balance updatesprivate.positions(options)— authenticated position reportsprivate.rfqDeliveries({ scope })— authenticated RFQ delivery events (scopeis"account"or"session")
Each session is created lazily on its first stream request and reused for subsequent streams in that category.
Isolated sessions
Partial-depth snapshot streams (depth()) each get their own WebSocketSession. This is because they connect to a different snapshot URL and their data semantics require a dedicated connection:
Code
When the stream is closed, its isolated session is torn down.
Order book session
orderBook() uses a separate shared session dedicated to order book management. All order book subscriptions multiplex over this one connection, but it is distinct from the public and authenticated sessions:
Code
The book session handles snapshot synchronization, diff application, and automatic resync — see the Order Book deep dive for details.
Connection count summary
| Usage pattern | Connections |
|---|---|
| 3 public streams | 1 (shared) |
| 3 public + 2 authenticated streams | 2 (public + authenticated) |
| 2 order books | 1 (book) |
1 public stream + 1 depth() + 1 order book | 3 (shared + isolated + book) |
Reconnect behavior
When a WebSocket connection drops, WebSocketSession reconnects automatically with exponential backoff.
Backoff parameters
WebSocket reconnect backoff uses equal jitter with these defaults. The policy is configurable through webSocketReconnect; the backoff client option continues to apply only to HTTP retries:
| Parameter | Value | Notes |
|---|---|---|
| Base delay | 250 ms | Configurable through webSocketBackoff.baseMs |
| Cap | 30 s | Configurable through webSocketBackoff.capMs |
| Factor | 2 | Configurable through webSocketBackoff.factor |
| Jitter | Equal jitter | Half fixed + half random |
| Retry attempts | 10 | Default maximum after a connection drop |
| Stable uptime | 30 s | Backoff resets only after this continuous uptime |
Attempt 0 reconnects immediately (delay = 0) — most drops are transient. Subsequent attempts grow exponentially: 250 ms → 500 ms → 1 s → ... capped at 30 s. Equal jitter prevents many clients from reconnecting in lockstep after an exchange restart. A connection that flaps before stable uptime does not reset its retry budget.
Configure the public client policy when an application needs a different bounded retry budget or a close-code classifier:
Code
Set unlimited: true only when the application deliberately owns the lifecycle of an indefinitely reconnecting stream. Authentication, policy, and unsupported-protocol close codes are treated as terminal by default. A connection timeout also cancels pending credential generation and fences the underlying socket attempt.
What happens on reconnect
- The socket closes (unexpectedly — not by
close()) WebSocketSessionschedules a reconnect after the backoff delay- If the session has authentication, fresh auth headers are generated via
headersFactory - A new socket is opened; stale sockets are ignored (late events from a superseded socket are discarded)
- On open, all durable subscriptions are replayed
- Streams transition:
"active"→"reconnecting"→"active"
If the reconnect policy is exhausted or a terminal close code is received, active streams transition to "failed" and expose the terminal error through lastError; they do not remain indefinitely in "reconnecting".
Pending requests reject on reconnect
One-shot request/response methods (public.ping(), public.time(), public.conninfo(), public.listSubscriptions(), public.depthSnapshot()) that are in-flight when a reconnect occurs are rejected immediately. They are not replayed — you must retry them yourself:
Code
Public and private connection state
Public and authenticated subscriptions use separate WebSocket connections. Connection-scoped controls are explicit: client.websocket.public owns the unauthenticated session, and client.websocket.private owns the authenticated server session.
Code
Mutations NEVER replay
Order placement, cancellation, and RFQ operations (private.placeOrder(), private.cancelOrder(), private.cancelAllOrders(), private.cancelSessionOrders(), private.rfq.submitQuote(), etc.) are classified as mutations. They are one-shot: rejected on disconnect, never retried or replayed. This is a safety guarantee — replaying a mutating request after reconnect could cause duplicate fills.
Subscription replay
Every SUBSCRIBE frame is stored by the transport. On reconnect, all stored subscriptions are re-sent to the fresh socket, restoring your streams without manual intervention.
Replay lifecycle
Code
Listen for replay events on individual streams:
Code
Stream states
A stream's .state property reflects its lifecycle:
| State | Meaning |
|---|---|
"active" | Connected and receiving data |
"reconnecting" | Connection dropped; waiting for reconnect and resubscription |
"failed" | An error occurred (check .lastError) |
"closed" | Explicitly closed by the caller |
Liveness checks
Long-lived WebSocket connections can go stale without a TCP close event (half-open connections, NAT timeouts, silent proxy drops). The SDK detects this with application-level ping/pong liveness checks.
Configuration
Pass webSocketLiveness when creating the client:
Code
How it works
- After each successful connection or ping response, a timer is scheduled at
intervalMs - When the timer fires, the session sends a
pingrequest over the WebSocket - If no response arrives within
timeoutMs, the connection is considered dead - The session forces a reconnect (triggering the normal reconnect/replay flow)
Without liveness configured, the SDK relies on the operating system's TCP keepalive and the exchange's server-side timeouts to detect dead connections.
Frame size limits
The SDK enforces a maximum inbound WebSocket message size to prevent memory exhaustion from malformed or malicious frames:
Code
Frames exceeding this limit are rejected: the SDK emits a ConnectionError, fires the error event on the transport, and closes the socket (which triggers a reconnect). The rejected frame is never delivered to stream listeners.
The size check uses UTF-8 byte length, not JavaScript string .length.
Diagnostic traffic classification
Every WebSocket diagnostic event is classified by traffic type for structured logging and observability:
| Classification | Events |
|---|---|
control | SUBSCRIBE, UNSUBSCRIBE, connection open/close, socket factory errors |
stream | Data frames (trades, depth updates, book tickers), malformed frames |
reconnect | Connection drop, reconnect scheduling, liveness failure |
mutation | Order placement/cancellation, RFQ operations |
Use the onDiagnostic callback to observe these:
Code
Each logical WebSocket request or subscription carries a stable event.correlationId across its lifecycle, including failures and subscription replay after reconnect. The returned stream exposes the subscription ID as stream.correlationId, so an orchestrator can attach application spans, metrics, and logs to the same stream without inspecting wire-level request IDs. REST and OAuth diagnostics expose their response correlation ID at the same top-level field.
Close behavior
Closing the client
client.close() tears down all WebSocket sessions — shared, book, and isolated:
Code
This is immediate and suppresses reconnection. Pending requests reject with "WebSocket session closed".
Closing a stream
stream.close() sends an UNSUBSCRIBE frame and waits for the server's acknowledgement before resolving. The stream's resources are released, but the shared session stays open for other streams:
Code
If the unsubscribe times out, the stream is still cleaned up locally. Pass a timeout to control the wait:
Code
Closing an order book
book.close() unsubscribes from the book session and releases the book's resources:
Code
Listener cleanup with AbortSignal
Stream listeners support AbortSignal for automatic cleanup without manual off() calls:
Code
Related
- WebSocket Reference — wire-format tables and subscription details
- Order Book deep dive — order book synchronization internals
- Data Types deep dive — bigint and decimal string handling in WebSocket frames
- Authentication — how WebSocket connections authenticate