redis/node-redis · error · Error
RedisSentinelClient lease already released
Error message
RedisSentinelClient lease already released
What it means
RedisSentinelClient.release() is idempotent-violating by design: calling it when #clientInfo is already undefined throws, because a double release would return the same master pool slot twice and corrupt the pool accounting. The lease is a single-use resource.
Source
Thrown at packages/client/lib/sentinel/index.ts:277
client => client.unwatch()
)
}
unwatch = this.UNWATCH;
/**
* Releases the client lease back to the pool
*
* After calling this method, the client instance should no longer be used as it
* will be returned to the client pool and may be given to other operations.
*
* @returns A promise that resolves when the client is ready to be reused, or undefined
* if the client was immediately ready
* @throws Error if the lease has already been released
*/
release() {
if (this._self.#clientInfo === undefined) {
throw new Error('RedisSentinelClient lease already released');
}
const result = this._self.#internal.releaseClientLease(this._self.#clientInfo);
this._self.#clientInfo = undefined;
return result;
}
}
export default class RedisSentinel<
M extends RedisModules,
F extends RedisFunctions,
S extends RedisScripts,
RESP extends RespVersions,
TYPE_MAPPING extends TypeMapping
> extends EventEmitter {
readonly _self: RedisSentinel<M, F, S, RESP, TYPE_MAPPING>;
#internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;View on GitHub (pinned to bb5beb5657)
Solutions
- Release exactly once; track a released flag if multiple code paths could call it.
- Prefer sentinel.use(fn) which handles acquire/release for you and cannot double-release.
- Null out the reference after release() to make accidental reuse obvious.
- Ensure finally blocks are not nested around the same lease.
Example fix
// before
const c = await sentinel.acquire();
try { ... } finally { await c.release(); }
await c.release(); // double release throws
// after
let released = false;
const c = await sentinel.acquire();
try { ... } finally { if (!released) { await c.release(); released = true; } } Defensive patterns
Strategy: validation
Validate before calling
let released = false;
const releaseOnce = async (c) => { if (!released) { released = true; await c.release(); } }; Try / catch
try { await c.release(); } catch (e) { if (!/already released/.test(e.message)) throw e; } Prevention
- Prefer sentinel.use(fn) to avoid manual release entirely.
- Track a released flag when multiple paths can release.
- Null the handle after release to make reuse obvious.
When it happens
Trigger: Calling release() twice on the same sentinel lease handle; calling release() in a finally block and again in a caller's finally; releasing the reserved client obtained via reserveClient:true twice.
Common situations: Nested try/finally blocks both releasing; acquire() result passed to a helper that releases and the caller also releases; error handling that releases then re-throws into another release.
Related errors
- Attempted execution on released RedisSentinelClient lease
- 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/034df378d716a0e8.json.
Report an issue: GitHub.