gastownhall/beads · error
timeout waiting for cache lock on %s
Error message
timeout waiting for cache lock on %s
What it means
acquireLock() polls a non-blocking exclusive flock on the cache entry's .lock file for up to 2 minutes (stale locks older than 5 minutes are removed first). This error means another process held the lock continuously for the whole deadline, so the operation was abandoned rather than mutating the dolt database concurrently.
Source
Thrown at internal/remotecache/cache.go:257
f, err := os.OpenFile(lp, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return nil, err
}
// Poll with timeout
deadline := time.Now().Add(2 * time.Minute)
for {
err := lockfile.FlockExclusiveNonBlocking(f)
if err == nil {
return f, nil
}
if !lockfile.IsLocked(err) {
_ = f.Close()
return nil, err
}
if time.Now().After(deadline) {
_ = f.Close()
return nil, fmt.Errorf("timeout waiting for cache lock on %s", remoteURL)
}
select {
case <-ctx.Done():
_ = f.Close()
return nil, fmt.Errorf("interrupted waiting for cache lock on %s: %w", remoteURL, ctx.Err())
case <-time.After(100 * time.Millisecond):
}
}
}
// releaseLock releases a cache entry file lock.
// The lock file is intentionally NOT removed: deleting it after unlock creates
// a TOCTOU race where another process's newly-acquired lock gets deleted.
// Stale lock files are cleaned up by acquireLock's age check instead.
func (c *Cache) releaseLock(f *os.File) {
if f != nil {
_ = lockfile.FlockUnlock(f)
_ = f.Close()View on GitHub (pinned to 71377f2769)
Solutions
- Identify and finish/kill the other process holding the lock (lsof/fuser on <cache-entry>/.lock).
- If no process is using it and the lock is genuinely stale-but-young, remove the .lock file (or wait out staleLockAge) and retry.
- Serialize usage: avoid running multiple bd commands against the same remote concurrently (scripts, cron overlap).
- Retry after a delay — this is a transient contention error, not data corruption.
Example fix
// before: immediate retry, same contention
if err := cache.Push(ctx, url); err != nil { return err }
// after: detect lock timeout and back off
if err := cache.Push(ctx, url); err != nil {
if strings.Contains(err.Error(), "timeout waiting for cache lock") {
return fmt.Errorf("another bd process is syncing %s; retry later", url)
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
lockPath := filepath.Join(cacheDir, "beads", "remotes", remotecache.CacheKey(remoteURL), ".lock")
if info, err := os.Stat(lockPath); err == nil {
age := time.Since(info.ModTime())
if age < 5*time.Minute {
return fmt.Errorf("cache lock active (%.0fs old); wait or stop the other process", age.Seconds())
}
} Try / catch
err := doSync(ctx) // Ensure/Push inside
if err != nil && strings.Contains(err.Error(), "timeout waiting for cache lock") {
// check no bd process is running, optionally clear a stale .lock, then retry once
select {
case <-time.After(time.Minute):
return doSync(ctx)
case <-ctx.Done():
return ctx.Err()
}
} Prevention
- Avoid overlapping scheduled jobs (cron/CI) that sync the same remote.
- Use a supervisor/mutex in your own tooling to serialize remote operations.
- Inspect `lsof <cache-entry>/.lock` before manually deleting a lock file.
- Keep sync durations short (fresh FreshFor TTL) so locks are released quickly.
When it happens
Trigger: Cache.Ensure() or Cache.Push() on a remoteURL whose .lock is held by another live process (long-running bd sync, stuck dolt command) for >2 minutes; the lock file is younger than staleLockAge so it is not treated as stale.
Common situations: Two shells running `bd sync`/`bd pull` on the same remote simultaneously; a hung dolt operation leaving a live-looking lock; backup/indexing software holding the file; very slow network making a legitimate push exceed 2 minutes.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- lock busy: held by another process
- lock already held by another process
- timeout (%s) waiting for spawn marker %s; wait for the in-pr
- lock already held by another process
- ErrLockHeld
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/03abe795e4533992.
Report an issue: GitHub.