ruvnet/ruflo · error · Error
timed out acquiring workspace-lease lock
Error message
timed out acquiring workspace-lease lock
What it means
WorkspaceLeaseRegistry serializes concurrent access with an O_EXCL lock file: it retries every 25ms against a 2-second deadline (Date.now() + 2000), breaking locks older than LOCK_STALE_MS = 10s. If the lock file still exists and is younger than 10s when the deadline passes, this timeout error is thrown. So the practical cause is another process holding the registry lock for over ~2 seconds.
Source
Thrown at v3/@claude-flow/cli/src/services/workspace-lease.ts:115
try {
const fd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
fs.writeSync(fd, String(process.pid));
fs.closeSync(fd);
try {
return fn();
} finally {
try { fs.unlinkSync(lockFile); } catch { /* already gone */ }
}
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
try {
const st = fs.lstatSync(lockFile);
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
fs.unlinkSync(lockFile);
continue;
}
} catch { /* raced — retry */ }
if (Date.now() > deadline) throw new Error('timed out acquiring workspace-lease lock');
await delay(25);
}
}
}
private readFile(repositoryId: string): LeaseFile {
const file = this.fileFor(repositoryId);
assertNotSymlink(file);
let parsed: LeaseFile = { version: 1, leases: {} };
if (fs.existsSync(file)) {
try {
const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
if (raw && typeof raw === 'object' && raw.leases && typeof raw.leases === 'object') {
parsed = { version: 1, leases: raw.leases };
}
} catch { /* corrupt — start fresh */ }
}
return parsed;View on GitHub (pinned to fa13ee4ad6)
Solutions
- Retry the lease operation after a short wait — the lock self-heals: it is force-removed once older than 10s (LOCK_STALE_MS)
- Ensure only one claude-flow process touches a given worktree's leases at a time (stop duplicate daemons: daemon stop, check pgrep)
- If you know no other process is running and the lock is stale, delete the lock file manually (the *.lock sibling of the registry file)
- Serialize lease operations in your own code instead of issuing them concurrently from several async paths
Example fix
// before
const lease = await registry.acquire(worktree, owner); // two processes at once -> timed out acquiring workspace-lease lock
// after
async function acquireWithRetry(registry, worktree, owner, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try { return await registry.acquire(worktree, owner); }
catch (e) {
if (!/timed out acquiring workspace-lease lock/.test(String(e?.message)) || i === attempts - 1) throw e;
await new Promise(r => setTimeout(r, 2000)); // lock goes stale at 10s
}
}
} Defensive patterns
Strategy: retry
Validate before calling
// Before lease work, confirm no live lock contention from other processes:
import * as fs from 'node:fs';
const lockFile = `${registryPath}.lock`;
try {
const st = fs.lstatSync(lockFile);
if (Date.now() - st.mtimeMs < 10_000) {
console.warn('Workspace-lease lock is fresh — another process likely holds it; deferring');
}
} catch { /* no lock file, uncontended */ } Try / catch
async function acquireLeaseWithRetry(acquire: () => Promise<unknown>, attempts = 3): Promise<unknown> {
for (let i = 0; i < attempts; i++) {
try { return await acquire(); }
catch (e) {
if (!/timed out acquiring workspace-lease lock/.test(String(e?.message))) throw e;
await new Promise(r => setTimeout(r, 2_000)); // lock force-expires at LOCK_STALE_MS = 10s
}
}
throw new Error('workspace-lease lock still held after retries');
} Prevention
- Serialize lease operations per worktree in your own orchestration layer instead of issuing them concurrently
- Ensure only one daemon/session is active per checkout (stop stray processes before batch runs)
- After a crash, allow ~10s for stale locks to age out, or remove the *.lock file manually when certain no process lives
When it happens
Trigger: Two or more claude-flow processes (daemon, hooks, CLI) acquiring/releasing workspace leases on the same repository simultaneously; a long GC pause or hung process holding the lock file; a lock left behind by a crashed process that is younger than the 10s stale threshold when you retry immediately.
Common situations: Parallel CI jobs on the same checkout; a session-start hook racing a manually triggered daemon command; retrying immediately after a crash whose lock file is still fresh; NFS/latency where unlink of the lock is slow.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out acquiring ai-budget lock
- timed out acquiring flywheel attempts lock
- timed out acquiring repo-supervisor lock
- timed out acquiring flywheel transaction lock
- Another Ruflo/MetaHarness installer still owns the Meta-Prox
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/02ee27a3199d9f8e.
Report an issue: GitHub.