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

  1. Use errors.Is(err, lockfile.ErrLockBusy) to detect contention and retry with backoff or skip gracefully
  2. Wait for the other process to finish, or serialize work so only one process locks the workspace at a time
  3. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/8c133797720faf02. Report an issue: GitHub.