redis/node-redis · warning · ScanIteratorInterruptedError
Scan iteration was interrupted by a Sentinel master change
Error message
Scan iteration was interrupted by a Sentinel master change
What it means
At the top of each scanIterator loop iteration, the code checks whether a MASTER_CHANGE topology event was observed since the last page. SCAN cursors are per-node server-side state, so a failover invalidates the cursor — the iterator aborts with ScanIteratorInterruptedError rather than silently returning stale or duplicated results.
Solutions
- Catch ScanIteratorInterruptedError and restart the scan from the beginning
- Use a larger COUNT to complete scans faster and narrow the failover window
- Track processed keys externally if resumable semantics are needed (SCAN has no server-side resume)
Example fix
// before
for await (const keys of sentinel.scanIterator()) {
console.log(keys);
}
// after
let done = false;
while (!done) {
try {
for await (const keys of sentinel.scanIterator()) {
console.log(keys);
}
done = true;
} catch (e) {
if (e instanceof ScanIteratorInterruptedError) {
console.warn('Scan interrupted by master change, restarting...');
continue;
}
throw e;
}
} Defensive patterns
Strategy: try-catch
Type guard
import { ScanIteratorInterruptedError } from 'redis';
function isScanInterrupted(e: unknown): e is ScanIteratorInterruptedError {
return e instanceof ScanIteratorInterruptedError;
} Try / catch
import { ScanIteratorInterruptedError } from 'redis';
for (;;) {
try {
for await (const keys of sentinel.scanIterator()) {
process(keys);
}
break;
} catch (e) {
if (e instanceof ScanIteratorInterruptedError) {
console.warn('Scan interrupted by master change, restarting...');
continue;
}
throw e;
}
} Prevention
- Always wrap scanIterator in a retry loop that catches ScanIteratorInterruptedError
- Use a larger COUNT to reduce the total scan duration and failover exposure
- Avoid scanning during planned Sentinel maintenance or rolling restarts
- Log interruptions to monitor failover frequency in your environment
When it happens
Trigger: A Sentinel failover (MASTER_CHANGE event) occurs between yielding one SCAN page and requesting the next. The topology-change listener sets masterChanged=true and the top-of-loop check fires on the next iteration.
Common situations: Failover during a long-running full-keyspace scan; planned maintenance or rolling restarts while iterating; Sentinel promoting a replica mid-scan.
Related errors
- no valid master node
- None of the sentinels are available
- SCAN: node serving this cursor has left the cluster —…
- already attempting to open
- Attempted execution on released RedisSentinelClient lease
AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11).
Data as JSON: /api/errors/4235c1c5ca0abc2a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/client/lib/sentinel/index.ts:723
* @throws {ScanIteratorInterruptedError} On observed `MASTER_CHANGE`.
*/
async *scanIterator(
this: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>,
options?: ScanOptions & ScanIteratorOptions
) {
let cursor: RedisArgument = options?.cursor ?? '0';
let masterChanged = false;
const handleTopologyChange = (event: RedisSentinelEvent) => {
if (event.type === 'MASTER_CHANGE') {
masterChanged = true;
}
};
this.on('topology-change', handleTopologyChange);
try {
do {
if (masterChanged) throw new ScanIteratorInterruptedError();
// Route through _execute so reserveClient:true reuses the reserved
// lease (instead of waiting forever on an empty master pool), and the
// lease is released before yielding — consumers can issue other
// commands inside the for-await loop without exhausting the pool.
let reply;
try {
reply = await this._execute(
false,
client => {
// Re-check after the lease resolves: a failover may have landed
// while waiting on an empty master pool, in which case the lease
// now points to a fresh client on the new master and SCAN would
// resume with a cursor from the old master.
if (masterChanged) throw new ScanIteratorInterruptedError();
return (client as RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>).scan(cursor, options);
}
);View on GitHub (pinned to 90fd0652bc)