# WebSocket Reconnection Strategy: Handling Dropped Connections Gracefully

> Learn proven WebSocket reconnection strategies—exponential backoff, jitter, state recovery, and client-side message queuing—to build resilient real-time apps.

- **Published:** 2025-04-19
- **Author:** Clixo
- **Reading time:** 6 min read
- **Tags:** websockets, reconnection, reliability, real-time, client-side
- **Canonical URL:** https://clixo.sh/blog/websocket-reconnection-strategy-exponential-backoff

A WebSocket connection drops. It might be a server restart, a network interruption, a load balancer timeout, or a user switching from WiFi to mobile data. The question is not whether this happens — it will, at production scale, continuously — but whether your application handles it gracefully or shows the user a broken, frozen UI.

A proper WebSocket reconnection strategy has three components: reconnect timing, state recovery, and message delivery. Most implementations address only the first.

## Why Reconnection Is Not Optional

The browser's `WebSocket` object does not automatically reconnect when a connection closes. The `close` event fires and that is it. If your code does nothing on close, your real-time feature silently stops working. The user has no indication this happened unless the UI fails visibly.

In practice, connection drops are not rare exceptions. They are normal operating conditions:

- Server deploys and rolling restarts close connections with `1001 Going Away`
- Load balancer idle timeouts close connections that have been quiet for a few minutes
- Mobile users routinely lose connectivity and regain it
- Browser tab backgrounding can trigger connection drops on some devices

Your reconnection logic is part of the product, not an edge-case handler.

## Exponential Backoff with Jitter

The naive reconnection strategy is to retry immediately on close. This creates a thundering herd: if a server restart drops ten thousand connections simultaneously, ten thousand clients hammer the server at the same instant, often before it has finished starting up.

Exponential backoff with jitter solves this. On each failed reconnection attempt, double the wait time and add a small random offset:

```js
class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.delay = 500;
    this.maxDelay = 30000;
    this.connect();
  }

  connect() {
    this.socket = new WebSocket(this.url);

    this.socket.addEventListener('open', () => {
      this.delay = 500; // reset on successful connect
      this.onOpen();
    });

    this.socket.addEventListener('message', (event) => {
      this.onMessage(event);
    });

    this.socket.addEventListener('close', (event) => {
      if (event.code !== 1000) {
        // not a deliberate close
        this.scheduleReconnect();
      }
    });
  }

  scheduleReconnect() {
    const jitter = Math.random() * 200;
    const waitMs = Math.min(this.delay + jitter, this.maxDelay);
    this.delay = Math.min(this.delay * 2, this.maxDelay);
    setTimeout(() => this.connect(), waitMs);
  }
}
```

Check the close code before reconnecting. A `1000` (normal closure) or a `4001` (unauthorized) should not trigger reconnection — those are intentional closes. A `1001`, `1006`, or a network error should.

```mermaid
stateDiagram-v2
  [*] --> Connecting
  Connecting --> Connected : open event
  Connected --> Disconnected : "close (1001, 1006, network)"
  Connected --> Done : close 1000
  Disconnected --> Backoff : schedule reconnect
  Backoff --> Connecting : delay expires
  Backoff --> Backoff : double delay (max 30s)
  Done --> [*]
```

## Handling the Auth Frame on Reconnect

If your server requires an authentication frame after connection, your reconnection logic must send it again. The new socket is a fresh TCP connection with no knowledge of the previous session:

```js
onOpen() {
  this.socket.send(JSON.stringify({
    type: 'auth',
    token: this.getAuthToken(),
  }));
}
```

If the token has expired during the disconnection window, your auth flow must handle refresh before reconnecting. Triggering a token refresh is the correct response to a `4001 Unauthorized` close code.

## State Recovery After Reconnection

Reconnecting restores the transport but not the application state. The server has no idea the client was gone or what it missed. Your reconnection strategy must address:

**What room or channel should the client rejoin?** The client must re-send its `join` messages after reconnection and re-establish auth. Store the subscription state locally and replay it on every new connection:

```js
onOpen() {
  this.sendAuth();
  // Re-subscribe to previously joined rooms
  this.subscriptions.forEach((roomId) => {
    this.socket.send(JSON.stringify({ type: 'join', roomId }));
  });
}
```

**What messages were missed?** If your server assigns sequence numbers to messages, the client can track the last sequence it received and request a replay on reconnect:

```js
onOpen() {
  this.sendAuth();
  this.socket.send(JSON.stringify({
    type: 'sync',
    lastSeq: this.lastReceivedSeq,
  }));
}
```

The server then sends all messages with a sequence number higher than `lastSeq`, or a state snapshot if too many messages were missed.

If your server does not support sequence-based replay, the fallback is a full-state refresh request on reconnect. This is more expensive but ensures consistency.

## Queuing Outbound Messages During Disconnection

Users continue interacting with your app while the connection is down. If those interactions trigger outbound messages that are silently dropped, state diverges. Queue outbound messages during disconnection and flush the queue on reconnect:

```js
send(message) {
  if (this.socket.readyState === WebSocket.OPEN) {
    this.socket.send(JSON.stringify(message));
  } else {
    this.outboundQueue.push(message);
  }
}

onReady() {
  // After auth and join are re-established
  while (this.outboundQueue.length > 0) {
    const msg = this.outboundQueue.shift();
    this.socket.send(JSON.stringify(msg));
  }
}
```

Be careful with queue size. If a user is offline for a long time, queuing unbounded messages is a memory risk. Set a maximum queue depth and surface an appropriate UI state ("You are offline — changes will sync when you reconnect") when the queue is full.

## UI Feedback During Disconnection

Your reconnection logic should expose state to the UI so users understand what is happening:

- **Disconnected** — show a banner or subtle indicator; do not leave the UI looking live when it is not
- **Reconnecting** — show a spinner or countdown
- **Reconnected** — briefly confirm that connection is restored, especially if the user was offline long enough to miss meaningful updates

The specific UI treatment depends on your product — a collaborative editor needs more prominent disconnection feedback than a live score ticker. The principle is the same: never silently fail.

## Using a Library vs Building from Scratch

Libraries like `reconnecting-websocket` (a thin wrapper over the native WebSocket API) handle the reconnection timing and backoff logic for you. Socket.IO includes reconnection, room management, fallback transports, and acknowledgment semantics — a substantial head start for teams building complex real-time features.

Building from scratch gives you more control and fewer dependencies, and it is not particularly complex once you understand the pieces. The value of understanding the reconnection mechanics from first principles is that you can debug issues in production regardless of which layer they appear in.

Resilient real-time products require deliberate design at every layer of the stack. If you are building real-time features that need to hold up under production conditions, [Clixo can help you ship them correctly](https://clixo.sh/#contact).

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
