Hyperliquid's /ws endpoint uses an application-level JSON heartbeat: the server sends {"method":"ping"} and expects {"method":"pong"}, and idle connections without traffic can be closed server-side. This is distinct from TCP keepalive and from WebSocket protocol-level ping/pong frames, and it matters because a socket can remain OPEN at the OS level while silently missing order updates. A robust client sends the documented ping on an interval, tracks a lastMessageAt timestamp, declares the connection dead when no pong or data arrives within a bounded window, and forces a reconnect with subscription replay. After a detected dead window, reconcile missed orderUpdates against REST state to close the gap.
Heartbeat Layers: TCP Keepalive, WebSocket Frames, and Hyperliquid JSON Ping/Pong
Three different keepalive mechanisms are often conflated. TCP keepalive is an OS-level probe that sends empty ACK packets to confirm the peer host is still reachable; it operates below the application and does not prove that your WebSocket session or its subscriptions are healthy. WebSocket protocol-level ping/pong frames are defined by RFC 6455 and are handled by the WebSocket implementation, typically without surfacing to application code. Hyperliquid adds a third layer: an application-level JSON heartbeat documented in the Hyperliquid Docs - Timeouts and heartbeats, where the /ws endpoint sends {"method":"ping"} and expects a {"method":"pong"} reply, and idle connections without traffic can be closed server-side. See RFC 6455 Section 5.5.2 (Ping and Pong frames) for the protocol-level definition.
Because these layers are independent, a passing TCP keepalive or a successful protocol-level pong does not guarantee that your application is receiving order updates. The Hyperliquid heartbeat is the contract that keeps the application session alive and gives you a signal to detect a dead peer. Treat it as the authoritative liveness check for your subscription wrapper, and treat the other two layers as transport hygiene rather than application health. The heartbeat format itself is documented in Hyperliquid's Timeouts and heartbeats reference.
- TCP keepalive: OS-level, proves host reachability, not session or subscription health.
- WebSocket protocol ping/pong: RFC 6455 frames, handled by the library, often invisible to app code.
- Hyperliquid JSON ping/pong: application-level, documented, and the signal your client should track.
Why an OPEN Socket Can Be a Zombie That Misses Order Updates
A half-open connection occurs when one side believes the session is alive but the other side has stopped delivering data. The local socket may still report OPEN because no FIN or RST was received, yet no orderUpdates, trades, or fills arrive. This is the classic zombie connection described in websocket.org - WebSocket Heartbeat: Ping/Pong and zombie connections. In trading, a zombie is worse than a clean disconnect because your code believes it is subscribed while fills silently go unprocessed.
Hyperliquid's documented behavior that idle connections without traffic can be closed server-side means the server may drop you during quiet periods, but the inverse is also dangerous: a network path can fail silently while the server still considers you connected. The only reliable way to distinguish a healthy quiet market from a dead socket is to require periodic proof of life. That proof is the JSON pong, optionally reinforced by any inbound data message.
- Zombie state: socket OPEN locally, no inbound messages, no error raised.
- Risk: missed orderUpdates and fills during the silent window.
- Mitigation: bounded liveness window based on pong or any data frame.
Subscription Wrapper Design: Ping Interval, lastMessageAt, and Dead Declaration
The wrapper should own four responsibilities: send the documented ping on an interval, update a lastMessageAt timestamp on every inbound frame, declare the connection dead when no pong or data arrives within a bounded window, and force a reconnect with subscription replay. Keep the ping interval shorter than the dead window so at least one ping can be answered before you declare failure. For example, ping every 15 seconds and declare dead after 45 seconds of silence, but tune these to your risk tolerance rather than copying values blindly.
Every inbound message, including pong and subscription data, should refresh lastMessageAt. This prevents false positives during active markets where data arrives continuously and pongs may be less frequent. When the dead window elapses, close the socket explicitly, clear timers, and trigger the reconnect path. The reconnect path must replay subscriptions; otherwise you reconnect to a silent socket with no channels. See Hyperliquid WebSocket subscriptions: connection lifecycle and reconnection for the lifecycle details this wrapper depends on.
- Ping interval must be shorter than the dead window.
- lastMessageAt refreshes on pong and on any data frame.
- Dead declaration closes the socket and starts reconnect plus replay.
Runnable Node.js Heartbeat Wrapper with Bounded Liveness Window
The following Node.js example uses the ws package and implements the documented JSON ping, a lastMessageAt tracker, a dead window, and subscription replay. It is intentionally minimal so you can adapt it to your order management logic. Replace the subscription payload with your actual channels, and wire the reconnect into your order reconciliation flow.
Note that the ping payload is the documented application-level message, not a WebSocket protocol frame. Sending it keeps the session alive and elicits a pong that proves the peer is responsive.
const WebSocket = require('ws');
const URL = 'wss://api.hyperliquid.xyz/ws';
const PING_INTERVAL_MS = 15000;
const DEAD_WINDOW_MS = 45000;
let ws;
let pingTimer;
let lastMessageAt = 0;
let subscriptions = [];
function connect() {
ws = new WebSocket(URL);
ws.on('open', () => {
lastMessageAt = Date.now();
for (const sub of subscriptions) ws.send(JSON.stringify(sub));
pingTimer = setInterval(() => {
if (Date.now() - lastMessageAt > DEAD_WINDOW_MS) {
console.error('dead connection detected, reconnecting');
return reconnect();
}
ws.send(JSON.stringify({ method: 'ping' }));
}, PING_INTERVAL_MS);
});
ws.on('message', (raw) => {
lastMessageAt = Date.now();
const msg = JSON.parse(raw.toString());
if (msg.method === 'pong') return;
handleMessage(msg);
});
ws.on('close', reconnect);
ws.on('error', (err) => console.error('ws error', err.message));
}
function reconnect() {
clearInterval(pingTimer);
if (ws) ws.terminate();
setTimeout(connect, 1000);
}
function handleMessage(msg) {
// route orderUpdates, trades, fills here
}
function subscribe(sub) {
subscriptions.push(sub);
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(sub));
}
connect();Half-Open Connection Detector and Forced Reconnect with Subscription Replay
A half-open detector is the logic that converts silence into action. It should run on a timer independent of the ping timer so that a stalled event loop or blocked send does not prevent detection. The detector compares Date.now() against lastMessageAt and, when the delta exceeds the dead window, marks the connection dead, terminates the socket, and schedules a reconnect. Terminating rather than gracefully closing avoids waiting on a peer that may never respond.
Subscription replay must be idempotent. Store subscriptions in a list and resend them on every open event. If your client uses subscription IDs, regenerate or reuse them consistently so you can correlate responses. After replay, your order reconciliation step should fetch current open orders and recent fills to close any gap created during the dead window. The sibling guide RPC WebSocket reconnect without data loss covers the gap recovery pattern in detail.
- Detector runs on its own timer, not inside the ping callback.
- Terminate the socket to avoid waiting on an unresponsive peer.
- Replay subscriptions on every open, then reconcile order state.
Reconciling Missed orderUpdates After a Detected Dead Window
When the detector fires, you have a known gap: from the last processed message to the moment the new connection is subscribed and streaming. During that window, orderUpdates may have been emitted and missed. The reconciliation pattern is to fetch authoritative state via REST after reconnect, compare it against your local order book, and apply any differences. This is safer than assuming the WebSocket replay will deliver historical updates, because the documented heartbeat and subscription model does not guarantee backfill of missed events.
For fills specifically, the trades and fills channel mechanics determine what you can reconstruct. Review Hyperliquid WebSocket trades and fills channel mechanics to understand which fields you can use for reconciliation. If your strategy depends on precise fill ordering, treat the dead window as a data gap and reconcile from REST rather than trusting local state.
- Mark the gap start at the last processed message timestamp.
- Fetch open orders and recent fills via REST after reconnect.
- Apply differences to local state before resuming live processing.
Measuring Your Own Heartbeat Behavior: Results Table Guidance
Do not rely on generic numbers for ping interval or dead window. Measure against your own endpoint and network path. Run a controlled test where you connect, subscribe to a low-traffic channel, and log the time between your ping and the pong, as well as the time between consecutive data messages. Then simulate a dead path by blocking traffic and record how long it takes your detector to fire.
Use a results table like the one below to record your observations. Fill it with your own measurements; the values shown are placeholders, not benchmarks. This method is reproducible and lets you tune the dead window to your risk tolerance without guessing.
- Ping-to-pong round trip: measure over at least 100 samples.
- Data inter-arrival time: measure during quiet and active markets.
- Detector fire time: measure after simulating a blocked path.
- False positive rate: count detector fires during healthy sessions.
| Metric | Sample 1 | Sample 2 | Notes |
| --- | --- | --- | --- |
| Ping-to-pong RTT (ms) | | | |
| Max data gap (s) | | | |
| Detector fire time (s) | | | |
| False positives | | | |Troubleshooting Persistent Disconnects and False Dead Declarations
If your client disconnects frequently, first check whether you are sending the documented ping at all. A client that never sends ping may be closed server-side during idle periods. Second, verify that your lastMessageAt updates on every inbound frame, not only on pong. A client that ignores data frames will falsely declare dead during active markets. Third, confirm that your ping interval is shorter than the dead window; otherwise you can declare dead before a pong has a chance to arrive.
If you see false dead declarations, increase the dead window or reduce the ping interval, and log the raw messages around the event. If disconnects correlate with specific error responses, review Hyperliquid API error handling and order rejections for rejection semantics that may indicate a malformed subscription rather than a transport failure. For latency-related symptoms, see Hyperliquid RPC latency to separate network delay from connection death.
- Missing ping: server may close idle connections.
- Stale lastMessageAt: update on all inbound frames.
- Interval longer than dead window: false positives.
- Malformed subscriptions: check error responses before blaming transport.
Limitations and Tradeoffs of Application-Level Heartbeats
Application-level heartbeats add overhead and complexity. Every ping is a message that consumes bandwidth and processing, and a short interval increases that cost. A long interval reduces overhead but increases the time to detect a dead connection, which directly increases the risk of missed fills. There is no universally correct setting; the tradeoff depends on your strategy's sensitivity to missed updates and your tolerance for false positives.
Another limitation is that the heartbeat proves the peer is responsive, not that your subscriptions are delivering data. A peer can answer pong while a specific channel is silent due to market conditions or a subscription issue. Combine the heartbeat with per-channel staleness checks if your strategy requires continuous data. Finally, the documented behavior that idle connections can be closed server-side means you cannot rely on a quiet socket staying open indefinitely; the heartbeat is mandatory, not optional.
- Short interval: faster detection, more overhead.
- Long interval: less overhead, higher missed-fill risk.
- Pong proves responsiveness, not per-channel data flow.
- Idle sockets may be closed server-side; heartbeat is required.
Next Steps: Hardening Your Hyperliquid WebSocket Client
Start by implementing the wrapper above and measuring your own ping-to-pong and data inter-arrival times. Then add per-channel staleness checks and a reconciliation step that runs after every reconnect. If you need managed endpoints with predictable behavior, review Hyperliquid RPC endpoints (RPC Assistant) and the API service for connection options. For network-level context, see Hyperliquid.
Finally, document your chosen intervals and dead window in your runbook, and test the detector regularly by simulating a blocked path. The OnFinality Learn hub contains related guides on subscriptions, fills, and reconnect recovery. If you are evaluating provider plans, RPC pricing outlines the options. The goal is not zero disconnects but zero missed fills: a detector that fires early and reconciles correctly is more valuable than a socket that never reconnects.
- Implement and measure before tuning.
- Add per-channel staleness and post-reconnect reconciliation.
- Test the detector by simulating a blocked path.
- Document intervals and dead window in your runbook.