# Deep Dive — WebSocket Sessions

The SDK manages WebSocket connections, reconnection, and subscription lifecycle automatically. Understanding the session architecture helps you reason about connection counts, reconnect behavior, and resource cleanup.

## Session architecture

Every WebSocket stream you open through `client.websocket` goes through a `WsSession`, which owns a `WsTransport` (the raw socket). The SDK uses three session categories:

### Shared session

Public streams and authenticated streams share **one** underlying `WsSession` (and therefore one `WsTransport`). These streams multiplex over a single connection:

- `trades(symbol)` — public trade stream
- `bookTicker(symbol)` — public best bid/ask
- `depthUpdates(symbol)` — public depth diffs
- `contractStatus()` — perpetual contract status
- `rfqs()` — public RFQ feed
- `orders(options)` — authenticated order updates
- `balances(options)` — authenticated balance updates
- `positions(options)` — authenticated position reports
- `rfqDeliveries(options)` — authenticated RFQ delivery events

The session is created lazily on the first stream request and reused for all subsequent ones.

### Isolated sessions

Partial-depth snapshot streams (`depth()`) each get their **own** `WsSession`. This is because they connect to a different snapshot URL and their data semantics require a dedicated connection:

```ts
// Each call creates a separate WebSocket connection
const depth5 = client.websocket.depth("BTCUSD", { levels: 5 });
const depth20 = client.websocket.depth("ETHUSD", { levels: 20 });
// That's 2 separate WebSocket connections, plus the shared session
```

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 main shared session:

```ts
const btcBook = client.websocket.orderBook("BTCUSD");
const ethBook = client.websocket.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](/tools/typescript-sdk/deep-dives/order-book) for details.

### Connection count summary

| Usage pattern | Connections |
|---|---|
| 3 public streams | 1 (shared) |
| 3 public + 2 authenticated streams | 1 (shared) |
| 2 order books | 1 (book) |
| 1 public stream + 1 `depth()` + 1 order book | 3 (shared + isolated + book) |

## Reconnect behavior

When a WebSocket connection drops, `WsTransport` reconnects automatically with exponential backoff.

### Backoff parameters

WebSocket reconnect backoff uses **fixed defaults** that are not configurable via client options (the `backoff` client option applies to HTTP retries only):

| Parameter | Value | Notes |
|---|---|---|
| Base delay | 250 ms | Fixed |
| Cap | 30 s | Fixed |
| Factor | 2 | Fixed |
| Jitter | Equal jitter | Half fixed + half random |

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.

### What happens on reconnect

1. The socket closes (unexpectedly — not by `close()`)
2. `WsTransport` 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"`

### Pending requests reject on reconnect

One-shot request/response methods (`ping()`, `time()`, `conninfo()`, `listSubscriptions()`, `depthSnapshot()`) that are in-flight when a reconnect occurs are **rejected** immediately. They are not replayed — you must retry them yourself:

```ts
try {
  const pong = await client.websocket.ping();
} catch (err) {
  // If the connection dropped mid-request, this rejects with:
  // "WebSocket session reconnecting"
}
```

### Mutations NEVER replay

Order placement, cancellation, and RFQ operations (`placeOrder()`, `cancelOrder()`, `cancelAllOrders()`, `cancelSessionOrders()`, `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

```
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:

```ts
const trades = client.websocket.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:

| 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:

```ts
import { createClient, HmacAuth } from "gemini-markets/server";

const client = await createClient({
  // ...
  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:

```ts
const client = await createClient({
  // ...
  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:

| 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:

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

## Close behavior

### Closing the client

`client.close()` tears down **all** WebSocket sessions — shared, book, and isolated:

```ts
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:

```ts
const trades = client.websocket.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:

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

### Closing an order book

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

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

### Listener cleanup with AbortSignal

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

```ts
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](/tools/typescript-sdk/reference/websocket) — wire-format tables and subscription details
- [Order Book deep dive](/tools/typescript-sdk/deep-dives/order-book) — order book synchronization internals
- [Data Types deep dive](/tools/typescript-sdk/deep-dives/data-types) — bigint and decimal string handling in WebSocket frames
- [Authentication](/tools/typescript-sdk/authentication) — how WebSocket connections authenticate
