ruvnet/ruflo · error · Error
timed out acquiring flywheel transaction lock
Error message
timed out acquiring flywheel transaction lock
What it means
Flywheel transactions serialize cross-process access with an O_EXCL lock file (`transaction-state.lock`): exclusive creation wins the lock, the holder unlinks it in `finally`, and a lock whose mtime is older than 60s (LOCK_STALE_MS) is treated as abandoned and reclaimed. When the lock exists, is still fresh, and the caller's deadline (LOCK_TIMEOUT_MS = 10s) expires while polling every 5ms, this error is thrown.
Solutions
- Serialize the runs — run one mutating flywheel command per checkout at a time and retry once the current holder finishes
- If no other process is active (`ps aux | grep claude-flow`), wait 60s for the stale-lock takeover, or manually delete `.claude-flow/flywheel-v1/transaction-state.lock` once the holder is confirmed dead
- Kill zombie claude-flow/daemon processes still holding the lock
- Give each CI shard its own workspace instead of sharing one checkout
Example fix
// before: fire-and-forget, no tolerance for a concurrent holder
await openFlywheelTransaction(root);
// after: bounded retry around the lock deadline
for (let attempt = 0; attempt < 3; attempt++) {
try { return await openFlywheelTransaction(root); }
catch (e) {
if (e?.message !== 'timed out acquiring flywheel transaction lock') throw e;
await new Promise((r) => setTimeout(r, 15_000)); // > LOCK_TIMEOUT_MS
}
}
throw new Error('flywheel transaction lock stayed contended'); Defensive patterns
Strategy: retry
Validate before calling
import { lstatSync, existsSync } from 'node:fs';
import { join } from 'node:path';
const LOCK_STALE_MS = 60_000;
// Best-effort preflight: report whether another holder is active.
function lockStatus(root: string): 'free' | 'stale' | 'held' {
const lock = join(root, '.claude-flow', 'flywheel-v1', 'transaction-state.lock');
if (!existsSync(lock)) return 'free';
const age = Date.now() - lstatSync(lock).mtimeMs;
return age > LOCK_STALE_MS ? 'stale' : 'held';
} Try / catch
const DEADLINE_MINUTES = 2;
const deadline = Date.now() + DEADLINE_MINUTES * 60_000;
let delayMs = 5_000;
while (true) {
try {
txn = await openFlywheelTransaction(root);
break;
} catch (e) {
if (e?.message !== 'timed out acquiring flywheel transaction lock') throw e;
if (Date.now() > deadline) throw e; // escalate after bounded retries
await new Promise((r) => setTimeout(r, delayMs));
delayMs = Math.min(delayMs * 2, 30_000);
}
} Prevention
- Run one mutating flywheel command per checkout at a time
- Do not share one workspace between parallel CI shards
- Clean up crashed claude-flow processes before starting new runs
- Remember the 60s staleness window: a killed process's lock blocks for up to a minute by design
When it happens
Trigger: Two or more claude-flow processes in the same checkout both open a flywheel transaction (parallel CI shards on one workspace, a forgotten interactive session, a hung daemon) and the second polls past its 10s deadline; or a crashed holder left a lock younger than 60s, so stale-takeover cannot fire yet.
Common situations: CI matrices that share one workspace; a long promotion running in one terminal while another flywheel command runs elsewhere; a killed process whose lock has not yet aged past the 60s staleness window.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out acquiring flywheel attempts lock
- timed out acquiring ai-budget lock
- timed out acquiring repo-supervisor lock
- timed out acquiring workspace-lease lock
- Another Ruflo/MetaHarness installer still owns the…
AI-assisted analysis of ruvnet/ruflo@5234333c34 (2026-08-18).
Data as JSON: /api/errors/e22466e7e9c976af.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/services/flywheel-transaction.ts:265
try {
const fd = fs.openSync(lock, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }), 'utf8');
fs.closeSync(fd);
try {
return await fn();
} finally {
try { fs.unlinkSync(lock); } catch { /* lock already gone */ }
}
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
try {
const stat = fs.lstatSync(lock);
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
fs.unlinkSync(lock);
continue;
}
} catch { /* raced with owner */ }
if (Date.now() >= deadline) throw new Error('timed out acquiring flywheel transaction lock');
await delay(5);
}
}
}
function validateReceiptId(receiptId: string): void {
if (!/^sha256:[a-f0-9]{64}$/.test(receiptId)) throw new Error('invalid receipt ID');
}
function receiptPath(root: string, receiptId: string): string {
validateReceiptId(receiptId);
return path.join(receiptDir(root), `${receiptId.slice('sha256:'.length)}.json`);
}
export function readFlywheelReceipt(root: string, receiptId: string): FlywheelEvaluationReceipt | null {
try {
const file = receiptPath(root, receiptId);
assertSafeFile(file);View on GitHub (pinned to 5234333c34)