gastownhall/beads · warning
lock busy: held by another process
Error message
lock busy: held by another process
What it means
ErrLockBusy is the sentinel error returned when a non-blocking lock (sync lock, flock, or file lock) cannot be acquired because another process holds a conflicting lock. Callers should test with errors.Is(err, lockfile.ErrLockBusy) (or lockfile.IsLocked) and treat it as an expected contention outcome, not an unexpected failure.
Source
Thrown at internal/lockfile/lock.go:12
package lockfile
import (
"errors"
)
// ErrLocked is returned when a lock cannot be acquired because it is held by another process.
var ErrLocked = errProcessLocked
// ErrLockBusy is returned when a non-blocking lock cannot be acquired
// because another process holds a conflicting lock.
var ErrLockBusy = errors.New("lock busy: held by another process")
// IsLocked returns true if the error indicates a lock is held by another process.
func IsLocked(err error) bool {
return errors.Is(err, errProcessLocked)
}
View on GitHub (pinned to 71377f2769)
Solutions
- Use errors.Is(err, lockfile.ErrLockBusy) to detect contention and retry with backoff or skip gracefully
- Wait for the other process to finish, or serialize work so only one process locks the workspace at a time
- Investigate for a stale lock holder (check the holder reported by ExclusiveHolder) and terminate it if it is orphaned
Example fix
// before
err := lockfile.AcquireSyncLock(path)
// treat as fatal
// after
if err := lockfile.AcquireSyncLock(path); err != nil {
if errors.Is(err, lockfile.ErrLockBusy) {
return nil // another process is syncing; skip
}
return err
} Defensive patterns
Strategy: retry
Type guard
func isLockBusy(err error) bool { return errors.Is(err, lockfile.ErrLockBusy) } Try / catch
for attempt := 0; attempt < 5; attempt++ {
err := lockfile.AcquireSyncLock(path)
if err == nil { break }
if !errors.Is(err, lockfile.ErrLockBusy) { return err }
time.Sleep(backoff(attempt))
} Prevention
- Always branch on errors.Is(err, lockfile.ErrLockBusy) before treating a lock error as fatal
- Serialize scheduled jobs (cron flock or single-instance supervisor) to avoid predictable contention
- Investigate persistent busy results — they usually indicate a stale or hung holder, not transient contention
When it happens
Trigger: AcquireSyncLock, FlockSharedNonBlock, FlockExclusiveNonBlock, Acquire, or ExclusiveHolder called on a lock file while another process holds it in a conflicting mode; the functions are non-blocking so they return immediately with ErrLockBusy instead of waiting.
Common situations: Two bd daemons/CLI invocations running sync on the same workspace concurrently; a stale process still holding the lock while a new one starts; cron jobs overlapping; CI running two pipelines against one checkout.
Related errors
- lock already held by another process
- lock already held by another process
- timeout waiting for cache lock on %s
- lock already held by another process
- ErrLockHeld
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/8c133797720faf02.
Report an issue: GitHub.