ruvnet/ruflo · error · Error
timed out acquiring ai-budget lock
Error message
timed out acquiring ai-budget lock
What it means
Thrown by GlobalAiBudget.acquireLock() when the O_EXCL lock file (~/.claude-flow/ai-budget.lock) cannot be acquired within a 2-second deadline. The budget fuse is intentionally strict — failure to account means denial — so lock contention directly blocks launches. Stale locks older than LOCK_STALE_MS are taken over automatically.
Source
Thrown at v3/@claude-flow/cli/src/services/global-ai-budget.ts:396
try {
const fd = fs.openSync(this.lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
fs.writeSync(fd, String(process.pid));
fs.closeSync(fd);
return () => {
try { fs.unlinkSync(this.lockFile); } catch { /* already gone */ }
};
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
// Stale lock from a crashed process — take over.
try {
const st = fs.lstatSync(this.lockFile);
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
fs.unlinkSync(this.lockFile);
continue;
}
} catch { /* raced — retry */ }
if (Date.now() > deadline) {
throw new Error('timed out acquiring ai-budget lock');
}
await delay(25);
}
}
}
/** Read + prune the ledger. Caller must hold the lock for read-modify-write. */
private readLedger(now: number): Ledger {
assertNotSymlink(this.ledgerFile);
let ledger: Ledger = { version: 1, launches: [], active: [] };
if (fs.existsSync(this.ledgerFile)) {
try {
const raw = JSON.parse(fs.readFileSync(this.ledgerFile, 'utf-8'));
if (raw && typeof raw === 'object') {
ledger = {
version: 1,
launches: Array.isArray(raw.launches) ? raw.launches.filter((l: LaunchRecord) => typeof l?.at === 'number') : [],
active: Array.isArray(raw.active) ? raw.active.filter((a: ActiveRecord) => typeof a?.at === 'number') : [],View on GitHub (pinned to 6b01dc5a68)
Solutions
- Reduce concurrent launch bursts (respect maxConcurrentGlobal).
- Identify the holder via the PID in ai-budget.lock and confirm it is alive.
- Move the budget dir to fast local storage (set RUFLO_AI_BUDGET_DIR).
- If the holder is a confirmed dead process and the lock is stale, remove it manually.
- As an EMERGENCY escape hatch only, set RUFLO_AI_BUDGET_DISABLE=1 (this disables all accounting).
Example fix
# diagnose ps -p "$(cat ~/.claude-flow/ai-budget.lock)" # if dead and stale, remove rm ~/.claude-flow/ai-budget.lock # or move budget dir to local storage export RUFLO_AI_BUDGET_DIR=/var/tmp/ai-budget
Defensive patterns
Strategy: retry
Validate before calling
function budgetLockStale(lockFile: string, staleMs: number): boolean {
try { return Date.now() - fs.lstatSync(lockFile).mtimeMs > staleMs; }
catch { return false; }
} Try / catch
try {
await budget.reserve(req);
} catch (e) {
if (e instanceof Error && /timed out acquiring ai-budget lock/.test(e.message)) {
// emergency only — otherwise surface a 'budget system busy' denial
throw new Error('AI budget lock contention — reduce concurrent launches or retry');
}
throw e;
} Prevention
- Set RUFLO_AI_BUDGET_DIR to fast local storage (not a network home dir).
- Respect maxConcurrentGlobal — do not launch more agents than the budget allows.
- Monitor for zombie processes holding ai-budget.lock.
- Keep RUFLO_AI_BUDGET_DISABLE=1 reserved for true emergencies only.
When it happens
Trigger: Multiple processes/ci jobs calling reserve() concurrently against the same budget dir; a process holding ai-budget.lock for >2s (slow I/O on a network home dir); heavy concurrent agent spawning across workspaces.
Common situations: Many parallel swarm agents launching from the same $HOME; a network-mounted home directory with high latency; a dead process whose lock hasn't aged past LOCK_STALE_MS yet; the ledger write/fsync being slow.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out acquiring flywheel transaction lock
- timed out acquiring flywheel attempts lock
- AI budget file is a symlink (refusing): ${path}
- Could not find repository root (no package.json found)
- no eligible peers for exploration
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/1ff21180a0facfd4.
Report an issue: GitHub.