GeminiGemini
Demo environmentGet API key
  • Overview
  • Crypto Trading
  • Prediction Markets
  • Perpetuals
  • Stocks
  • API Reference
  • SDKs & Tools
Changelog
Gemini logoGemini logo

© 2026 Gemini Space Station, Inc.

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 stream
  • public.bookTicker(symbol) — public best bid/ask
  • public.depthUpdates(symbol) — public depth diffs
  • public.contractStatus() — perpetual contract status
  • public.rfqs() — public RFQ feed
  • private.orders({ scope }) — authenticated order updates (scope is "account" or "session")
  • private.balances(options) — authenticated balance updates
  • private.positions(options) — authenticated position reports
  • private.rfqDeliveries({ scope }) — authenticated RFQ delivery events (scope is "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
// Each call creates a separate WebSocket connection const depth5 = client.websocket.public.depth("BTCUSD", { levels: 5 }); const depth20 = client.websocket.public.depth("ETHUSD", { levels: 20 }); // That's 2 separate WebSocket connections, plus any public and authenticated shared sessions

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
const btcBook = client.websocket.public.orderBook("BTCUSD"); const ethBook = client.websocket.public.orderBook("ETHUSD"); // Both share the same book session (1 connection), separate from the main session

The book session handles snapshot synchronization, diff application, and automatic resync — see the Order Book deep dive for details.

Connection count summary

Usage patternConnections
3 public streams1 (shared)
3 public + 2 authenticated streams2 (public + authenticated)
2 order books1 (book)
1 public stream + 1 depth() + 1 order book3 (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:

ParameterValueNotes
Base delay250 msConfigurable through webSocketBackoff.baseMs
Cap30 sConfigurable through webSocketBackoff.capMs
Factor2Configurable through webSocketBackoff.factor
JitterEqual jitterHalf fixed + half random
Retry attempts10Default maximum after a connection drop
Stable uptime30 sBackoff 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
const client = await createClient({ env: "sandbox", webSocketReconnect: { maxAttempts: 5, stableConnectionMs: 30_000, shouldReconnect: ({ closeCode }) => closeCode !== 1008, }, });

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

  1. The socket closes (unexpectedly — not by close())
  2. WebSocketSession schedules a reconnect after the backoff delay
  3. If the session has authentication, fresh auth headers are generated via headersFactory
  4. A new socket is opened; stale sockets are ignored (late events from a superseded socket are discarded)
  5. On open, all durable subscriptions are replayed
  6. 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
try { const pong = await client.websocket.public.ping(); } catch (err) { // If the connection dropped mid-request, this rejects with: // "WebSocket session reconnecting" }

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
const publicSubscriptions = await client.websocket.public.listSubscriptions(); const privateSubscriptions = await client.websocket.private.listSubscriptions();

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
socket closes → backoff delay → new socket opens → replay all subscriptions → await ACK for each → "resubscribed" event fires per stream

Listen for replay events on individual streams:

Code
const trades = client.websocket.public.trades("BTCUSD"); trades.on("resubscribed", () => { console.log("Subscription restored after reconnect"); }); trades.on("subscriptionError", (err) => { console.error("Resubscription failed:", err); // The stream state is now "failed" });

Stream states

A stream's .state property reflects its lifecycle:

StateMeaning
"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
import { createClient, HmacAuth } from "@gemini-markets/sdk/server"; const client = await createClient({ env: "sandbox", // ... webSocketLiveness: { intervalMs: 30_000, // ping every 30 seconds (default) timeoutMs: 10_000, // expect pong within 10 seconds (default) }, });

How it works

  1. After each successful connection or ping response, a timer is scheduled at intervalMs
  2. When the timer fires, the session sends a ping request over the WebSocket
  3. If no response arrives within timeoutMs, the connection is considered dead
  4. 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
const client = await createClient({ env: "sandbox", // ... webSocketMaxMessageSizeBytes: 1_048_576, // 1 MB (default) });

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:

ClassificationEvents
controlSUBSCRIBE, UNSUBSCRIBE, connection open/close, socket factory errors
streamData frames (trades, depth updates, book tickers), malformed frames
reconnectConnection drop, reconnect scheduling, liveness failure
mutationOrder placement/cancellation, RFQ operations

Use the onDiagnostic callback to observe these:

Code
const client = await createClient({ env: "sandbox", // ... onDiagnostic: (event) => { if (event.traffic === "reconnect") { metrics.increment("ws.reconnect"); } }, });

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
client.close(); // All sessions closed, all pending requests rejected, all streams emit "close"

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
const trades = client.websocket.public.trades("BTCUSD"); await trades.ready; // wait for subscription ACK // Later... await trades.close(); // sends UNSUBSCRIBE, waits for ACK, then resolves

If the unsubscribe times out, the stream is still cleaned up locally. Pass a timeout to control the wait:

Code
await trades.close({ timeoutMs: 5_000 });

Closing an order book

book.close() unsubscribes from the book session and releases the book's resources:

Code
const book = client.websocket.public.orderBook("BTCUSD"); // ... book.close(); // unsubscribes, releases memory

Listener cleanup with AbortSignal

Stream listeners support AbortSignal for automatic cleanup without manual off() calls:

Code
const controller = new AbortController(); trades.on("message", (trade) => { console.log(trade.p, trade.q); }, { signal: controller.signal }); // Later: remove the listener without a reference to the callback controller.abort();

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
On this page
  • Session architecture
    • Shared sessions
    • Isolated sessions
    • Order book session
    • Connection count summary
  • Reconnect behavior
    • Backoff parameters
    • What happens on reconnect
    • Pending requests reject on reconnect
    • Public and private connection state
    • Mutations NEVER replay
  • Subscription replay
    • Replay lifecycle
    • Stream states
  • Liveness checks
    • Configuration
    • How it works
  • Frame size limits
  • Diagnostic traffic classification
  • Close behavior
    • Closing the client
    • Closing a stream
    • Closing an order book
    • Listener cleanup with AbortSignal
  • Related
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript