ruvnet/ruflo · error
Storage is locked by another process (holder: ${lockData.hol
Error message
Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). Lock file: ${this.lockPath} What it means
EventStore.acquireLock() implements single-writer file locking: if the lock file exists, parses as JSON, and is younger than LOCK_STALE_MS (30 seconds), it throws with the recorded holder and age. Locks older than 30s are treated as stale and deleted, and unparseable/corrupted lock files are also removed — so this error means a genuinely recent lock: another process is writing, or a crashed process died less than 30s ago.
Source
Thrown at v3/@claude-flow/guidance/src/persistence.ts:221
};
}
/**
* Acquire a file-based lock for concurrent access prevention.
* Throws if the lock is already held by another process.
*/
async acquireLock(): Promise<void> {
await this.ensureDirectory();
// Check for stale locks
if (existsSync(this.lockPath)) {
try {
const lockContent = await readFile(this.lockPath, 'utf-8');
const lockData = JSON.parse(lockContent);
const age = Date.now() - lockData.timestamp;
if (age < LOCK_STALE_MS) {
throw new Error(
`Storage is locked by another process (holder: ${lockData.holder}, age: ${age}ms). ` +
`Lock file: ${this.lockPath}`
);
}
// Stale lock, remove it
await unlink(this.lockPath);
} catch (err) {
if (err instanceof Error && err.message.startsWith('Storage is locked')) {
throw err;
}
// Corrupted lock file, remove it
try { await unlink(this.lockPath); } catch { /* ignore */ }
}
}
const holder = randomUUID();
const lockData = { holder, timestamp: Date.now(), pid: process.pid };
await writeFile(this.lockPath, JSON.stringify(lockData), 'utf-8');View on GitHub (pinned to fa13ee4ad6)
Solutions
- Serialize all writes to a storage path through a single owner process
- Retry acquireLock() with backoff — locks older than 30s self-heal via the stale check
- If you can verify the holder process is dead (holder PID), manually remove the lock file; otherwise wait out the stale window
- Give each concurrent process its own storagePath so no contention exists
Example fix
// before
await store.acquireLock(); // throws if another writer holds it
// after
async function acquireWithRetry(store: EventStore, tries = 5, delayMs = 2_000) {
for (let i = 0; i < tries; i++) {
try { return await store.acquireLock(); }
catch (err) {
if (i === tries - 1 || !String(err.message).startsWith('Storage is locked')) throw err;
await new Promise(r => setTimeout(r, delayMs));
}
}
} Defensive patterns
Strategy: retry
Validate before calling
import { existsSync, readFileSync } from 'node:fs';
function lockAgeMs(lockPath: string): number | null {
if (!existsSync(lockPath)) return null; // free
try {
const { timestamp } = JSON.parse(readFileSync(lockPath, 'utf-8'));
return Date.now() - timestamp;
} catch {
return 0; // corrupted: acquireLock will clear it
}
}
const age = lockAgeMs(store['lockPath'] ?? lockPath);
if (age !== null && age < 30_000) {
// another writer is active; wait or route to the owning process
} Try / catch
async function withLock<T>(store: EventStore, fn: () => Promise<T>): Promise<T> {
const maxTries = 10;
for (let i = 0; ; i++) {
try {
await store.acquireLock();
break;
} catch (err) {
const msg = err instanceof Error ? err.message : '';
if (!msg.startsWith('Storage is locked') || i === maxTries - 1) throw err;
await new Promise(r => setTimeout(r, Math.min(1000 * 2 ** i, 30_000)));
}
}
try { return await fn(); } finally { await store.releaseLock(); }
} Prevention
- Serialize writers: one owning process per storage directory
- Always pair acquireLock() with releaseLock() in a finally block
- Locks self-heal after 30s (LOCK_STALE_MS); only remove a lock file manually after confirming the holder is dead
When it happens
Trigger: Two processes using the same storage directory calling save()/append() concurrently; the previous writer crashed moments ago and left its lock file; a long-running write still holding the lock while a second one starts.
Common situations: Multiple workers/CLI invocations pointed at one storagePath; orchestrators restarting a crashed job immediately; tests running in parallel against shared fixture storage.
Related errors
- swarm state is busy; retry the outcome update
- timed out acquiring ai-budget lock
- timed out acquiring flywheel attempts lock
- timed out acquiring repo-supervisor lock
- timed out acquiring workspace-lease lock
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/0589f9732d34e5ab.
Report an issue: GitHub.