redis/node-redis · error · Error

RedisSentinelClient lease already released

Error message

RedisSentinelClient lease already released

What it means

release() was called on a RedisSentinelClient lease that was already released. The #clientInfo field is already undefined (set by the first release), so a second call is rejected to prevent corrupting pool state or double-returning the lease.

Solutions

  1. Guard release with a boolean flag to ensure it is called exactly once
  2. Use try/finally with release only in the finally block, never in the try body
  3. Null out the lease reference after release as a visual indicator it is spent

Example fix

// before
const lease = await sentinel.getClient();
await lease.release();
await lease.release(); // throws

// after
const lease = await sentinel.getClient();
let released = false;
try {
  // ... work
} finally {
  if (!released) {
    released = true;
    await lease.release();
  }
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard release with a boolean flag to prevent double-release.
let released = false;
const lease = await sentinel.getClient();

function safeRelease() {
  if (released) return;
  released = true;
  return lease.release();
}

try {
  // ... work
} finally {
  await safeRelease();
}

Try / catch

try {
  await lease.release();
} catch (e) {
  if (e.message === 'RedisSentinelClient lease already released') {
    // Already released — safe to ignore
  }
}

Prevention

When it happens

Trigger: Calling lease.release() twice — e.g., an explicit release followed by a finally block that calls release again; error-handling code that releases then the normal path also releases.

Common situations: try/finally where release is in the finally and also in the try body; cleanup logic in multiple error handlers both calling release; async race where two code paths release the same lease.

Related errors


AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11). Data as JSON: /api/errors/034df378d716a0e8. Report an issue: GitHub.

Appendix: 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 90fd0652bc)