mastra-ai/mastra · error · AgentThreadLeaseConflictError

AgentThreadLeaseConflictError(runId, owner)

Error message

AgentThreadLeaseConflictError(runId, owner)

What it means

After the in-process active-run check, the runtime acquires a distributed lease on the thread key via a lease provider. If the lease could not be acquired, another owner (run) already holds it, and AgentThreadLeaseConflictError is thrown with the conflicting runId and current owner. This guards thread exclusivity across processes/instances.

Source

Thrown at packages/core/src/agent/thread-stream-runtime.ts:1333

    threadId: string,
    resourceId: string | undefined,
    registrationOptions: AgentThreadStrictRegistrationOptions,
  ): Promise<AgentThreadRunRegistration> {
    await registrationOptions.validate?.();

    const state = this.#getState(pubsub);
    this.#sweepStaleSuspendedRecords(state, pubsub);
    const key = this.#threadKey(resourceId, threadId);
    const activeRunId = state.activeThreadRunIds.get(key);
    const activeRecord = activeRunId ? state.threadRunsById.get(activeRunId) : undefined;
    if (activeRecord && activeRecord.runId !== output.runId && this.#isThreadBlockingRun(state, activeRecord)) {
      throw new Error(`Cannot register run ${output.runId}: thread is already active with run ${activeRunId}`);
    }
    const resolvedPubSub = this.#getPubSub(pubsub);
    const leaseProvider = this.#getLeaseProvider(resolvedPubSub);
    const lease = await leaseProvider.acquireLease(key, output.runId, AGENT_THREAD_LEASE_TTL_MS);
    if (!lease.acquired) {
      throw new AgentThreadLeaseConflictError(output.runId, lease.owner ?? 'another owner');
    }

    // A failed external-ownership validation means another recovery attempt may
    // already own this same runId. Do not release its indistinguishable thread
    // lease; its TTL or the successor registration will take over renewal.
    await registrationOptions.validate?.();

    this.#startLeaseRenewal(resolvedPubSub, key, output.runId);
    const { streamId, streamSeq } = this.#nextStreamIdentity(state, output.runId);
    const {
      output: outputForSubscribers,
      createSubscriberStream,
      startBroadcast,
      cancelBroadcast,
      broadcastFinished,
    } = this.#withBroadcastStream(output, pubsub, key, streamId);
    const record: AgentThreadRunRecord<OUTPUT> = {
      agent,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Retry the run after the current lease expires (lease TTL is AGENT_THREAD_LEASE_TTL_MS); catch AgentThreadLeaseConflictError and back off.
  2. Serialize per-thread work through a queue so only one run targets a thread at a time.
  3. Ensure the owner releases its lease on cancellation/completion so new runs don't wait for TTL.
  4. Check your lease provider setup (e.g. shared Redis/DB) — instances must point at the same lease store.
  5. Route requests for the same thread to the same instance (sticky routing) to reduce conflicts.

Example fix

// before
await runtime.registerRun(output); // throws if lease held
// after
try {
  await runtime.registerRun(output);
} catch (e) {
  if (e instanceof AgentThreadLeaseConflictError) await waitForLease(e.owner);
  else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

// check whether the lease is currently held before attempting
const lease = await leaseProvider.acquireLease(threadKey, runId, TTL);
if (!lease.acquired) console.warn(`thread busy, owned by ${lease.owner}`);

Try / catch

try {
  runtime.registerRun(output);
} catch (e) {
  if (e instanceof AgentThreadLeaseConflictError) {
    await delay(backoff);
    return registerWithRetry(output); // bounded retries until lease TTL expires
  }
  throw e;
}

Prevention

When it happens

Trigger: Two processes/instances (or two runs) attempting to stream the same thread concurrently: leaseProvider.acquireLease returns acquired=false because another run owns the lease within AGENT_THREAD_LEASE_TTL_MS.

Common situations: Horizontal scaling with multiple server instances hitting the same thread; a previous run's lease not yet expired after a crash (must wait for TTL); clock/redis misconfiguration making leases behave unexpectedly; replayed requests.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/d07812638cd15c71. Report an issue: GitHub.