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
- Retry the run after the current lease expires (lease TTL is AGENT_THREAD_LEASE_TTL_MS); catch AgentThreadLeaseConflictError and back off.
- Serialize per-thread work through a queue so only one run targets a thread at a time.
- Ensure the owner releases its lease on cancellation/completion so new runs don't wait for TTL.
- Check your lease provider setup (e.g. shared Redis/DB) — instances must point at the same lease store.
- 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
- Implement bounded exponential backoff retries around run registration
- Ensure all instances share the same lease provider configuration
- Release leases promptly on cancellation/completion
- Use sticky routing per thread to avoid cross-instance contention
- Keep lease TTLs appropriate for expected run durations
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
- Factory kickoff lease was lost before completion.
- DURABLE_AGENT_RECOVER_ALREADY_IN_PROGRESS
- Cannot register run ${output.runId}: thread is already activ
- MastraFactory.prepare() called twice
- ${file} is in use by another process — is another Mastra Cod
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/d07812638cd15c71.
Report an issue: GitHub.