PolynomialHQdocs
Guides

Connections and reconnection

Display connection state, configure retry behavior, and handle errors.

The client connects when a room is entered and can automatically retry temporary failures.

Take the network down, edit while it is offline, and bring it back:

Loading the connection demo…

Observe status

The native room status has seven states:

StatusMeaning
idleNo connection attempt has started
connectingA transport is being opened
connectedThe WebSocket is open and the room join is in progress
joinedThe room is ready
reconnectingA retry is scheduled or in progress
closedThe connection was intentionally closed
errorThe connection stopped on an error
import { useRoomStatus } from "@polynomialhq/react";

function ConnectionBadge() {
  const status = useRoomStatus();

  return (
    <span data-status={status}>
      {status === "joined" ? "Live" : status === "reconnecting" ? "Reconnecting…" : status}
    </span>
  );
}

useStatus() collapses the same machine into five coarser values:

useRoomStatus()useStatus()
idleinitial
connecting, connectedconnecting
joinedconnected
reconnectingreconnecting
closed, errordisconnected

useConnectionSnapshot() also exposes connectionId, userId, roomId, role, and lastError.

Configure retries

Set reconnection on the client for every room:

const client = createClient({
  authEndpoint: "/api/polynomial-auth",
  reconnect: {
    delayMs: (attempt) => Math.min(500 * 2 ** attempt, 10_000),
    enabled: true,
    maxAttempts: 8,
  },
});

Or override it for one room:

<RoomProvider
  id="document:42"
  initialPresence={{ cursor: null }}
  initialStorage={{ blocks: [] }}
  reconnect={{
    delayMs: 1_000,
    enabled: true,
    maxAttempts: 5,
  }}
>
  <Editor />
</RoomProvider>

reconnect: true enables the default retry policy and reconnect: false disables it. The defaults are enabled: true, unlimited maxAttempts, and an exponential backoff of Math.min(1000 * 2 ** (attempt - 1), 10_000) — one second, then two, four, eight, and ten seconds from then on. attempt starts at 1.

Retries only cover transport failures. A non-retryable protocol error such as unauthorized or forbidden moves the room to error and stops reconnecting, because retrying the same rejected token would not help. Mint a new token and re-enter the room.

Your authEndpoint is called again on every reconnect attempt, so an expired token is replaced automatically as long as the endpoint keeps returning fresh ones.

Listen for connection loss

import { useLostConnectionListener } from "@polynomialhq/react";

useLostConnectionListener((event) => {
  if (event === "lost") {
    showToast("Connection lost. Retrying…");
  }
  if (event === "restored") {
    showToast("Back online.");
  }
  if (event === "failed") {
    showToast("Could not reconnect.");
  }
});

lost fires once when the room enters reconnecting, restored when it reaches joined again, and failed when the room lands in error. Pass a stable callback — wrap it in useCallback if it closes over state — because the listener is re-registered whenever its identity changes.

Listen for errors

import { useErrorListener } from "@polynomialhq/react";

useErrorListener((error) => {
  reportError({
    code: error.code,
    message: error.message,
    retryable: error.retryable,
  });
});

Possible protocol codes are unauthorized, forbidden, not_found, invalid_message, rate_limited, and internal_error. Transport-level failures arrive without a code and are always retryable.

What happens to state during a reconnect

  • Others are cleared as soon as the room leaves joined, so cursors disappear immediately rather than freezing in place.
  • Presence is re-sent automatically once the room rejoins.
  • Storage stays readable throughout. Local mutations keep working and queue as pending Yjs updates, which are flushed after the room rejoins.
  • Broadcasts sent while disconnected are lost. There is no replay buffer.

Keep the last rendered document on screen during a reconnect. Replacing the editor with a spinner throws away work the user can still see.

Lower-level snapshots

Framework-agnostic code can subscribe to the room:

const unsubscribe = room.subscribe((event) => {
  if (event.type === "connection.changed") {
    console.log(event.snapshot.connection);
  }

  if (event.type === "error") {
    console.error(event.error);
  }
});

For transport-level integrations, use createRoomConnection() and subscribe to RoomConnectionEvent.

Pending storage updates

The lower-level RoomConnection tracks outgoing Yjs updates until the server acknowledges them:

connection.getPendingYjsUpdateCount();

connection.subscribe((event) => {
  if (event.type === "yjs.outbox.changed") {
    setPendingSaveCount(event.pendingYjsUpdateCount);
  }
});

Use useStorageStatus() in React unless you specifically need the raw queue count.

On this page