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

  1. Issue all commands inside the sentinel.use(async c => {...}) callback and never leak c out of it.
  2. If using acquire(), call release() exactly once and never touch the client afterward.
  3. Do not cache the leased client in a long-lived variable.
  4. 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

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


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