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

  1. Reduce concurrent launch bursts (respect maxConcurrentGlobal).
  2. Identify the holder via the PID in ai-budget.lock and confirm it is alive.
  3. Move the budget dir to fast local storage (set RUFLO_AI_BUDGET_DIR).
  4. If the holder is a confirmed dead process and the lock is stale, remove it manually.
  5. 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

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

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/1ff21180a0facfd4. Report an issue: GitHub.