CORE JSC

International Technology Partnership

Web Development & SEO

Fixing WebSocket Connections That Silently Stop Receiving Messages After the Device Wakes From Sleep

A live chat or notification feature works fine while the user is actively using the device, but after a laptop wakes from sleep or a phone returns from being locked for a while, new messages just stop arriving — no error, no visible disconnect, the UI still shows "connected." The socket didn't fail gracefully; it's a stale connection the client never found out was already dead.

Core JSC Team·October 1, 2026
Web DevelopmentWebSocketReal-TimeNetworkingJavaScript

The Problem

A WebSocket-based feature — live chat, real-time notifications, a collaborative editing indicator — works correctly during active use. After a laptop sleeps and wakes, or a mobile browser tab sits backgrounded for an extended period, new messages stop arriving entirely. No onclose or onerror event fires, the client's own connection-state variable still says "connected," and the UI shows no indication anything is wrong. The only way a user discovers it is by manually refreshing the page, at which point everything works again.

Why It Happens

A sleeping device's network interface disconnects without giving the TCP connection a clean close

When a laptop sleeps or a phone's OS aggressively suspends background network activity, the underlying TCP connection a WebSocket relies on doesn't go through a normal close handshake — it simply stops being able to send or receive. From the client JavaScript's perspective, nothing happened: no close event, no error, because the browser-level WebSocket object has no way to know the network layer underneath it died silently.

Intermediate network infrastructure (NAT, load balancers) can drop an idle connection without notifying either endpoint

Independent of device sleep, NAT routers, corporate proxies, and load balancers commonly have idle-connection timeouts that silently drop a TCP connection that hasn't sent traffic recently, without sending a proper FIN/RST to either side. A long-idle WebSocket — one where neither side happens to be actively messaging — can be quietly dropped at this intermediate layer well before any device-level sleep is even involved.

Without application-level heartbeats, there's no mechanism to detect a connection that looks open but isn't actually delivering data

A WebSocket connection object reporting readyState === WebSocket.OPEN only reflects what the client last knew — it's not a live, continuously-verified health check. Without the application itself periodically confirming round-trip delivery, there's no way to distinguish a genuinely healthy idle connection from one that silently died at the network layer.

A reconnection strategy that only triggers on an explicit close event never fires for this specific failure mode

Many WebSocket reconnection implementations listen for the close event and reconnect when it fires — which is correct for a clean disconnect, but does nothing for a connection that never actually closes from the client's perspective, because the underlying network died without ever triggering that event in the first place.

The Fix

1. Implement an application-level ping/pong heartbeat independent of the browser's connection state

function startHeartbeat(socket) {
  let awaitingPong = false;

  const interval = setInterval(() => {
    if (awaitingPong) {
      socket.close(); // no pong received in time — treat as dead and force a reconnect
      return;
    }
    awaitingPong = true;
    socket.send(JSON.stringify({ type: "ping" }));
  }, 30000);

  socket.addEventListener("message", (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "pong") awaitingPong = false;
  });

  socket.addEventListener("close", () => clearInterval(interval));
}

A regular application-level ping that expects a matching pong within a timeout window actively verifies the connection is genuinely delivering data round-trip, rather than trusting the client's cached readyState — a missed pong is a reliable signal to force-close and reconnect, independent of whether the browser ever fires its own close event.

2. Detect page visibility and device wake events to proactively re-verify the connection

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "visible") {
    verifyConnectionHealth(); // e.g. send an immediate ping, or just reconnect defensively
  }
});

Listening for the page becoming visible again — a reasonable proxy for "the device likely just woke up or the tab was backgrounded" — gives a natural trigger point to actively re-check connection health immediately, rather than waiting for the next scheduled heartbeat interval to eventually catch the problem.

3. Make the reconnection logic trigger on heartbeat failure, not only on the close event

function connectWithReconnect() {
  const socket = new WebSocket(url);
  startHeartbeat(socket); // heartbeat failure calls socket.close() internally
  socket.addEventListener("close", () => {
    setTimeout(connectWithReconnect, getBackoffDelay()); // reconnect on any close, heartbeat-triggered or not
  });
  return socket;
}

Having the heartbeat mechanism itself call close() on failure means the existing close-triggered reconnection logic gets invoked for this specific failure mode too — the two mechanisms compose rather than needing entirely separate reconnection paths for a clean close versus a silently dead connection.

4. Resync any state that may have been missed during the dead period after reconnecting

socket.addEventListener("open", () => {
  requestMissedMessages(lastKnownMessageId); // fetch anything sent while disconnected
});

Reconnecting alone isn't sufficient if messages were sent by the server during the period the connection was silently dead — requesting a resync of anything since the last confirmed message, rather than assuming the new connection picks up exactly where the old one left off, prevents a gap in message history from going unnoticed.

Why This Works

Each fix addresses a different part of why a silently-dead connection goes undetected and unrecovered. An application-level heartbeat actively verifies round-trip delivery instead of trusting a cached connection-state flag; visibility-change detection gives an immediate trigger point rather than waiting out the full heartbeat interval; routing heartbeat failures through the existing reconnection logic means one consistent recovery path handles both clean and silent disconnects; and resyncing missed state after reconnecting closes the gap a simple reconnect alone would leave in message history.

Conclusion

A WebSocket that silently stops receiving messages after a device sleeps isn't a bug in the reconnection logic itself — it's the absence of any mechanism to detect a connection that looks open but has actually died at the network layer, which a browser's close event alone can't catch. Add an application-level heartbeat that verifies round-trip delivery, use page visibility changes as a trigger to proactively re-check connection health, route heartbeat failures into the same reconnection path as a clean close, and resync any messages missed during the dead period once reconnected.