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

  1. Release exactly once; track a released flag if multiple code paths could call it.
  2. Prefer sentinel.use(fn) which handles acquire/release for you and cannot double-release.
  3. Null out the reference after release() to make accidental reuse obvious.
  4. 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

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


AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03). Data as JSON: /data/errors/034df378d716a0e8.json. Report an issue: GitHub.