redis/node-redis · error · Error
Attempted execution on released RedisSentinelClient lease
Error message
Attempted execution on released RedisSentinelClient lease
What it means
RedisSentinelClient._execute guards against use after release: once release() sets the internal #clientInfo to undefined, any subsequent command routed through _execute throws. The leased sentinel client is a short-lived handle tied to a master pool slot; after release it has no backing client and must not be used.
Source
Thrown at packages/client/lib/sentinel/index.ts:184
S,
RESP,
K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING
>;
}
/**
* Override the `typeMapping` command option
*/
withTypeMapping<TYPE_MAPPING extends TypeMapping>(typeMapping: TYPE_MAPPING) {
return this._commandOptionsProxy('typeMapping', typeMapping);
}
async _execute<T>(
isReadonly: boolean | undefined,
fn: (client: RedisClient<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>) => Promise<T>
): Promise<T> {
if (this._self.#clientInfo === undefined) {
throw new Error("Attempted execution on released RedisSentinelClient lease");
}
return await this._self.#internal.execute(fn, this._self.#clientInfo);
}
async sendCommand<T = ReplyUnion>(
isReadonly: boolean | undefined,
args: CommandArguments,
options?: CommandOptions,
): Promise<T> {
const mergedOptions = { ...this.commandOptions, ...options };
return this._execute(
isReadonly,
client => client.sendCommand(args, mergedOptions)
);
}
/**View on GitHub (pinned to bb5beb5657)
Solutions
- Issue all commands inside the sentinel.use(async c => {...}) callback and never leak c out of it.
- If using acquire(), call release() exactly once and never touch the client afterward.
- Do not cache the leased client in a long-lived variable.
- After an error, obtain a fresh lease rather than reusing the old handle.
Example fix
// before
const c = await sentinel.acquire();
await c.release();
await c.get('k'); // throws
// after
await sentinel.use(async c => { await c.get('k'); }); // scope owns the lease Defensive patterns
Strategy: validation
Validate before calling
await sentinel.use(async c => { await c.get('k'); }); // scope guarantees liveness Type guard
function leaseAlive(c) { return c != null && typeof c.release === 'function' && c.isOpen; } Try / catch
// Not a catch scenario — design the lease to be used only inside use()/acquire()-release.
Prevention
- Use sentinel.use(fn) so the lease never escapes scope.
- Treat release() as terminal; never call the handle again.
- Obtain a fresh lease after any error instead of reusing.
When it happens
Trigger: Calling any command on a RedisSentinelClient obtained via sentinel.use(fn) or sentinel.acquire() after the callback returned or release() was called. Using a stale reference captured before a topology reset.
Common situations: Forgetting that use()'s callback is the only safe scope; storing the leased client in a variable and calling it later; double-using a client after an error path already released it; async code holding the reference across an await that follows release.
Related errors
- RedisSentinelClient lease already released
- already attempting to open
- TokenManager is not running, but refresh was called
- TokenManager is not running, but a new token was received
- TokenManager is not running but received an error: ${errorMe
AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03).
Data as JSON: /data/errors/4c19fa3176de3002.json.
Report an issue: GitHub.