Live connections drop after a minute of quiet
“Busy sockets stay up for hours, but a socket with no traffic closes after almost exactly 60 or 100 seconds, usually with code 1006 and no close reason. It never happens on localhost.”
ERROR CODES YOU MAY SEE
A code is a clue. Use its meaning and surrounding evidence to narrow the cause.
1006WebSocket close codes · RFC 6455 · standardThe connection ended abnormally without a received Close frame. This is a local indication; endpoints must not send 1006 in a Close frame.
Check network interruptions, proxy timeouts, and process restarts before choosing a reconnect and state-recovery strategy.
Official reference for 1006 (opens in new tab)Code definition reviewedECONNRESETNode.js system errors · platformThe peer forcibly closed the connection.
Correlate client, proxy, and server logs. A reset alone does not reveal whether an operation completed before the connection ended.
Official reference for ECONNRESET (opens in new tab)Code definition reviewed
THE PRINCIPLE
An idle WebSocket is only as long-lived as the shortest idle timeout on its path, so a heartbeat is part of the protocol, not an optimization.
FIRST MOVES
- Measure the time from the last message to the close. 60s points to nginx (proxy_read_timeout default 60s) or an AWS ALB (idle timeout default 60s). About 100s is commonly reported behind Cloudflare, which doesn't publish its WebSocket idle limit.
- Have the server send protocol pings every 20-30s. Browsers answer them automatically, and the bytes reset idle timers on every hop.
- Pings in the browser are invisible to JavaScript, and JS can't send them. If the client must notice a dead link, add an app-level heartbeat message with a timeout.
- Raise the timeouts you own (proxy_read_timeout, ALB idle_timeout.timeout_seconds, 1-4000s) as a backstop, but keep the heartbeat: you don't control carrier NATs or corporate proxies.
- Allow a generous heartbeat deadline on the client: browsers throttle timers in hidden tabs, so client-scheduled work can run late.
TOOLS: THEN → NOW
PATTERN SNAPSHOT
// Server (Node ws): ping every 25s, kill sockets that missed a pong.
const alive = new WeakSet<WebSocket>(); // answered since the last ping
const timer = setInterval(() => {
for (const ws of wss.clients) {
if (!alive.has(ws)) { ws.terminate(); continue; }
alive.delete(ws);
ws.ping();
}
}, 25_000);
wss.on("connection", (ws) => {
alive.add(ws);
ws.on("pong", () => alive.add(ws));
});
wss.on("close", () => clearInterval(timer));CLOSE THE AI. EXPLAIN THIS.
The browser answers protocol pings automatically. Why might you still need an application-level heartbeat?HOW IT WORKS UNDERNEATH
RELATED SYMPTOMS
SOURCES
Guide reviewed