Why does a React Native app need an application-level heartbeat on its WebSocket, even though React Native's WebSocket has a ping() method?
answer
- can JavaScript see a pong?
- Android client has no read timeout
- a message the server must answer
- deadline, then close and reconnect
- pause the timer in the background
basics
~20 sReact Native's ping() sends a protocol ping, but no pong reaches JavaScript, and Android's client has no read timeout, so a dead socket can look OPEN. An app-level heartbeat the server answers within a deadline lets your code detect it and reconnect.
solid answer
~50 sLiveness has to be observable by the code that will act on it. React Native's `ws.ping()` asks the native module to send a protocol ping frame, but JavaScript gets no pong event, so it proves nothing to your code. The Android client is also built with no read timeout, so a half-open connection after a network switch or a NAT timeout can sit in `OPEN` indefinitely. The fix is an application heartbeat: every N seconds, send a small message such as `{"type":"ping","id":42}`, expect the server's matching reply within a deadline, and if it does not arrive, close the socket and let the reconnect logic take over. Any inbound message can count as proof of life to save traffic. The heartbeat only runs while the app is active, and its interval is a trade-off between detection speed, battery and server load.
code
typescript · 26 linesexport function startHeartbeat(ws: WebSocket, onDead: () => void) {
let id = 0;
let deadline: ReturnType<typeof setTimeout> | undefined;
const armDeadline = () => {
clearTimeout(deadline);
deadline = setTimeout(onDead, 35_000);
};
const interval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
id += 1;
ws.send(JSON.stringify({ type: 'hb', id }));
}
}, 25_000);
const onMessage = () => armDeadline(); // any inbound frame proves life
ws.addEventListener('message', onMessage);
armDeadline();
return () => {
clearInterval(interval);
clearTimeout(deadline);
ws.removeEventListener('message', onMessage);
};
}go deeper
Recall that a socket can look open while dead, and that a regular message the server answers is how the app finds out.
Explain half-open connections and why React Native's ping() cannot help your code: no pong reaches JavaScript and Android has no read timeout.
Implement a heartbeat with a deadline, reset on any inbound message, paused in the background and owned by the same manager as reconnects.
Set heartbeat policy across client and server: interval versus battery and fleet load, server-side eviction of silent clients, and per-feature urgency.
## The problem a heartbeat solves A WebSocket can fail in a way neither side notices: the **half-open connection**. The phone switches networks, a NAT gateway drops an idle mapping, or the server restarts behind a load balancer. No close frame is delivered, the TCP connection simply stops carrying data, and React Native keeps reporting `readyState === OPEN`. Two details of React Native's implementation make this more likely to linger: - **Android has no read timeout.** React Native's Android WebSocket client sets a connect and write timeout of 10 seconds but disables the read timeout, so waiting for data never times out on its own. - **Pongs are invisible.** React Native adds a non-standard `ws.ping()` that sends a protocol ping frame through the native module. The server's pong is handled natively and **no event reaches JavaScript**, so your code cannot time it. The protocol-level ping may still help keep intermediaries from treating the connection as idle, but it cannot tell your code that the connection is dead. ## An application-level heartbeat Move liveness into the messages your code can see: 1. **Send** a small heartbeat message on an interval, for example every 25 seconds: `{ type: 'hb', id: 17 }`. 2. **Expect** the server's reply with the same id within a deadline, for example 10 seconds. 3. **Count other traffic.** Any inbound message proves the connection is alive, so reset the deadline on every `onmessage` and skip heartbeats while bids are streaming. 4. **On a miss**, call `ws.close()` and hand over to the reconnect logic; do not wait for the OS to notice. 5. **Measure** the round trip if you want a connection-quality indicator in the UI. The server side needs a matching rule: drop clients that stay silent for longer than a few heartbeat intervals, which frees resources held by phones that vanished. ## Choosing the interval | Interval | Detection | Cost | |---|---|---| | 5 s | Very fast | More radio wake-ups and battery; more server load | | 25-30 s | A dead socket noticed within about half a minute | Balanced for interactive apps | | 2-5 min | Slow; users see stale data | Cheap, but may be longer than NAT idle timeouts | For a live auction, stale data costs money, so a shorter interval is justified while a lot is active; a chat inbox can afford longer. ## Mobile-specific rules - **Pause in the background.** Timers do not run reliably in a suspended app, and waking the radio for heartbeats drains battery. Stop the heartbeat when the app leaves `active`, and on return send one immediately as a foreground health check. - **Clear timers on close.** A heartbeat timer that outlives its socket will send on a closed connection or trigger a second reconnect. - **One owner.** Keep the heartbeat in the same connection manager that owns reconnects, so there is one state machine instead of racing timers. ## Designing the heartbeat message Keep the heartbeat boring and cheap: - **Small and typed.** A tiny JSON object with a `type` and a counter is enough; the server replies with the same counter so the client can match reply to request. - **Separate from business messages.** Handle heartbeat replies before your normal message dispatch, so the auction logic never sees them. - **Server-initiated variant.** Some backends send the heartbeat and expect the client to answer. That works equally well, as long as the client keeps a deadline for how long it may go without hearing anything. - **Log misses, not beats.** Record a missed deadline with the connection's age and network type; logging every beat just adds noise. A heartbeat miss followed by a successful reconnect is normal on mobile. A pattern of misses on one network type or app version is a signal worth investigating. ## What interviewers look for They want to hear **half-open connections**, why **`readyState` is not proof of life**, why React Native's **`ping()` does not help your code** (no pong in JavaScript), and a heartbeat with **a deadline and a reconnect on miss** that is **paused in the background**.
- Why not just rely on the operating system's TCP keepalive?TCP keepalive intervals are typically far longer than an interactive app can tolerate, are not configurable from React Native's JavaScript, and report nothing to your code. An application heartbeat gives a deadline you choose and a callback you control.
- Your heartbeat fires onDead right after the app returns from the background. Is that a bug?Not necessarily. If the deadline timer was armed before suspension, it may fire as soon as JavaScript resumes. Better: stop the heartbeat when the app leaves the active state, and on return send one immediately and arm a fresh deadline, so the verdict reflects the current connection.
React Native's ping() is like posting a letter with no return address: it may reach the other side, but you will never know. A heartbeat is a phone call with 'say OK if you hear me', and hanging up to redial when nobody answers.
saying these in an interview costs you the question
- ws.ping() lets JavaScript measure whether the server is still alive.
- readyState OPEN proves the connection can still deliver messages.
- React Native's Android WebSocket times out reads after a default period.
- Heartbeats should keep running while the app is in the background.
- A heartbeat interval of a few seconds is free on a phone.