> For the complete documentation index, see [llms.txt](https://docs.reya.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.reya.xyz/developers/devnet/connectivity/heartbeats.md).

# Heartbeats

Reya's WebSocket APIs (both [Info](/developers/devnet/api-reference/websocket-api-reference.md) and [Order Entry](/developers/devnet/api-reference/ws-exec-api-reference.md)) use **identical** heartbeat mechanics. This page is the single source of truth for connection liveness and close codes on both surfaces.

**For most clients, no action is required.** Any standards-compliant WebSocket library handles heartbeats automatically at the protocol layer: the server issues protocol-level pings, the client library replies with pongs, and the connection stays alive with no application-level code — no ping handler, no pong to send, no timer to track. The one exception is clients behind aggressive network middleboxes; see [Behind a strict middlebox?](#behind-a-strict-middlebox).

## The Two Layers

There are two distinct "ping/pong" concepts in a WebSocket connection, and confusing them is a common source of integration bugs.

### Layer 1 — Protocol-level heartbeat (RFC 6455 control frames)

This is the mechanism Reya uses to detect dead connections. It runs **below the application layer** ([RFC 6455 §5.5.2](https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2)): the server sends a **PING control frame** (opcode `0x9`), and every standards-compliant client library automatically replies with a **PONG control frame** (opcode `0xA`). If the client is dead (TCP disconnected, process crashed, network gone), no PONG arrives and the server closes the connection after the idle timeout.

This layer is **invisible to your application code** — your `onmessage` / `on_message` handler is never invoked for protocol pings or pongs.

### Layer 2 — Application-level JSON ping (optional)

A *separate*, *optional* feature: clients may send `{"type":"ping"}` and the server replies `{"type":"pong"}`. **This does not keep the connection alive** — Layer 1 already does that. Use Layer 2 only for measuring application-level round-trip latency, correlating a probe to its reply via the `id` field, or asserting the backend is processing application messages (not just that the TCP socket is up). Otherwise ignore it. See [The Optional Application-Level Probe](#the-optional-application-level-probe).

### Heartbeats are not Cancel-on-Disconnect

Neither layer is Cancel-on-Disconnect (COD). Liveness is governed by the protocol-level heartbeat and idle timeout here; COD is a **separate, account-scoped armed timer** refreshed **only** by sending `cancelAllAfter` — order-entry traffic, pings, and the socket itself do not touch it, and a dropped or idle socket does **not** fire COD. See [Cancel-on-Disconnect](/developers/devnet/order-entry/cancel-on-disconnect.md).

## Heartbeat and idle timeout

Both WebSocket APIs send protocol-level pings. Your WebSocket library should reply with pongs automatically.

A connection closes after **120 seconds without inbound traffic**, including protocol pong replies. Keep heartbeat handling enabled and reconnect with backoff if the connection closes.

## Behind a Strict Middlebox?

Some networks (corporate firewalls, mobile carriers, aggressive load balancers) drop "idle" TCP connections after \~30–60 seconds. Reya's server-sent pings are paced for the server's 120-second timeout, not for the most aggressive middleboxes. If you hit spurious disconnects through such a network, configure your **client** to send its own protocol-level pings at a shorter interval — one line of config in every common library:

**Python (`websocket-client`):**

```python
ws.run_forever(ping_interval=20, ping_timeout=10)
```

**Python (`websockets`, async):**

```python
async with websockets.connect(url, ping_interval=20, ping_timeout=10) as ws:
    ...
```

**Node (`ws`):**

```javascript
setInterval(() => ws.ping(), 20_000);  // ws library auto-handles pong replies
```

**Rust (`tokio-tungstenite`):**

```rust
// Send a Ping control frame manually
ws.send(Message::Ping(vec![])).await?;
```

**Browser (vanilla `WebSocket`):** The browser handles protocol-level pings internally and exposes no API to send them from JavaScript. If a browser-based client gets dropped by a middlebox, the only knob is application-level: send a `{"type":"ping"}` JSON frame periodically (see below).

The "20 seconds" figure is conservative — fast enough for almost any middlebox, light enough to be invisible in traffic. Adjust if your environment requires something more aggressive.

## The Optional Application-Level Probe

If you need Layer 2, here's the wire format. Client sends:

```json
{ "type": "ping", "id": "probe-001" }
```

* `type`: required, literal `"ping"`.
* `id`: optional free-form string, echoed back on the reply for correlation; omit it for a plain round-trip check.

Server replies:

```json
{ "type": "pong", "id": "probe-001" }
```

The `id` is echoed back **only if** the client sent one. Active servers do not supply a `timestamp`; measure round-trip latency from your own ping send time. Canonical Info AsyncAPI 3.4.0 still permits an optional `timestamp` on ping and pong, while Order Entry does not, so portable clients should use only `type` and `id` and must not treat the optional field as a server-time source. Layer 2 is purely additive — it runs in parallel with Layer 1 and doesn't disable it.

**Do not send `{"type":"pong"}` from your client.** Client-initiated JSON pong frames are invalid and receive an "Invalid type" / `UNKNOWN_TYPE` error. The only `pong` direction is server→client, in reply to a client `{"type":"ping"}`.

## What Happens When the Server Closes the Connection

The server can close a connection for several reasons; you'll see different close codes depending on cause:

| Close code               | Meaning                                                 | Recommended client action                                                            |
| ------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `1000`                   | Normal closure (you or the server called close cleanly) | Reconnect if you still want a connection                                             |
| `1001`                   | Order Entry service restarting                          | Reconnect after a short backoff; reconcile any unanswered request before retrying    |
| `1012`                   | Info connection must be re-established                  | Reconnect and re-subscribe for a fresh snapshot; treat the reason string as advisory |
| `1013`                   | Info client is not keeping up with updates              | Reconnect and re-subscribe; make sure the consumer can keep up before resuming       |
| `1006` / `1011` / others | Idle timeout, server error, abrupt closure              | Reconnect with exponential backoff                                                   |

If the connection is still desired, reconnect with backoff. On Info, replay subscriptions and rebuild from the new snapshots. On Order Entry, reconcile any unanswered request against REST/Info state before retrying; acceptance by the matching engine and on-chain settlement are separate outcomes.

For the surface-specific post-reconnect steps, see [Reconnection Pattern](/developers/devnet/api-reference/websocket-api-reference.md#reconnection-pattern) on the Info WebSocket reference and [Reconnection Pattern](/developers/devnet/api-reference/ws-exec-api-reference.md#reconnection-pattern) on the Order Entry WebSocket reference.

### Service restarts

During a service restart:

* **Info WebSocket:** connections close with code `1012`. Reconnect and re-subscribe for fresh snapshots.
* **Order Entry WebSocket:** new requests may receive `SERVER_SHUTTING_DOWN`, and connections close with code `1001`. Requests already sent may have completed; reconcile any unanswered request before resubmitting it.

Both codes are soft reconnect signals, but their recovery state differs: Info rebuilds subscriptions from snapshots; Order Entry reconciles request outcomes.
