redis/node-redis · warning · ClientOfflineError
The client is offline
Error message
The client is offline
What it means
Thrown by RedisClusterSlots.#assertReady() when #isOpen is true but #isReady is false. The cluster is open (connect() was called) but has not finished discovering topology and establishing node clients — or it lost readiness during a rediscovery. Commands are rejected because the slot map is not yet populated enough to route correctly.
Solutions
- Await cluster.connect() fully and listen for the 'ready' event before issuing commands.
- Guard with if (cluster.isReady) or retry with backoff until isReady is true.
- Ensure connect() did not throw; if it did, re-connect before issuing commands.
- During topology refreshes, queue or retry commands rather than assuming continuous readiness.
Example fix
// before
cluster.connect(); // not awaited
cluster.set('k', 'v'); // throws ClientOfflineError
// after
await cluster.connect(); // resolves when ready
if (!cluster.isReady) await once(cluster, 'ready');
await cluster.set('k', 'v'); Defensive patterns
Strategy: validation
Validate before calling
if (!cluster.isReady) {
await once(cluster, 'ready');
} Type guard
function clusterIsReady(cluster: RedisClusterType): boolean {
return cluster.isReady;
} Try / catch
while (!cluster.isReady) {
await new Promise(r => setTimeout(r, 50));
}
await cluster.set('k', 'v'); Prevention
- Await connect() and confirm isReady before issuing commands.
- Listen for the 'ready' event during startup.
- Retry/backoff if a command hits ClientOfflineError during topology refresh.
When it happens
Trigger: Issuing a command immediately after cluster.connect() resolves but before the 'ready'/'connect' event fires (rare). Issuing commands during a topology refresh that temporarily unset #isReady. Issuing commands right after connect() failed partway and left the cluster open but not ready.
Common situations: Not awaiting the connect()/ready event fully; race between command issuance and initial discovery completion; the cluster connect threw after setting #isOpen but the caller continued issuing commands; reconnect logic issuing commands before rediscovery completes.
Related errors
- Cluster already open
- Cluster closed
- FT.CURSOR: unknown cursor
- The client is closed
- All replies must be array of numbers for logical AND…
AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11).
Data as JSON: /api/errors/8ef074e5aee4405e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/client/lib/cluster/cluster-slots.ts:995
promises.push(fn(this.pubSubNode.client));
this.pubSubNode = undefined;
}
this.#resetSlots();
this.nodeByAddress.clear();
this.#reconnectionTracker.clear();
await Promise.allSettled(promises);
this.#emit('disconnect');
}
#assertReady() {
if (!this.#isOpen) {
throw new ClientClosedError();
}
if (!this.#isReady) {
throw new ClientOfflineError();
}
}
/**
* All fan-out target nodes (masters + replicas), WITHOUT connecting. The
* caller connects each node lazily in its own per-node promise so a single
* failed connect rejects only that node's execution — letting reducers such
* as `one_succeeded` still see the reachable shards — instead of a `Promise.all`
* over the connects failing the whole route up front. Excludes the dedicated
* PubSub connection (not in `masters`/`replicas`).
*/
getAllNodes() {
this.#assertReady();
return [...this.masters, ...this.replicas];
}
/** Master fan-out target nodes, WITHOUT connecting (see {@link getAllNodes}). */View on GitHub (pinned to 90fd0652bc)