gastownhall/beads · warning

lock already held by another process

Error message

lock already held by another process

What it means

errProcessLocked is the Windows sentinel returned when a non-blocking exclusive LockFileEx (LOCKFILE_EXCLUSIVE_LOCK|LOCKFILE_FAIL_IMMEDIATELY) cannot acquire the lock because another process holds it. Like the Unix variant, it is matched portably through lockfile.IsLocked / errors.Is(err, lockfile.ErrLockBusy).

Source

Thrown at internal/lockfile/lock_windows.go:13

//go:build windows

package lockfile

import (
	"errors"
	"os"
	"syscall"

	"golang.org/x/sys/windows"
)

var errProcessLocked = errors.New("lock already held by another process")

// flockExclusive acquires an exclusive non-blocking lock on the file using LockFileEx
func flockExclusive(f *os.File) error {
	// LOCKFILE_EXCLUSIVE_LOCK (2) | LOCKFILE_FAIL_IMMEDIATELY (1) = 3
	const flags = windows.LOCKFILE_EXCLUSIVE_LOCK | windows.LOCKFILE_FAIL_IMMEDIATELY

	// Create overlapped structure for the entire file
	ol := &windows.Overlapped{}

	// Lock entire file (0xFFFFFFFF, 0xFFFFFFFF = maximum range)
	err := windows.LockFileEx(
		windows.Handle(f.Fd()),
		flags,
		0,          // reserved
		0xFFFFFFFF, // number of bytes to lock (low)
		0xFFFFFFFF, // number of bytes to lock (high)
		ol,
	)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Handle it as expected contention via lockfile.IsLocked(err): retry with backoff or skip
  2. Identify the holder (Resource Monitor / handle.exe on the lock file) and close or kill the stale process
  3. Ensure holders release locks deterministically (defer unlock) and avoid long-lived exclusive holds

Example fix

// before
err := lockfile.Acquire(path)
if err != nil { return err } // fatal on busy
// after
if err := lockfile.Acquire(path); err != nil {
    if lockfile.IsLocked(err) { return nil } // busy: retry later or skip
    return err
}
Defensive patterns

Strategy: retry

Type guard

func isWindowsLockBusy(err error) bool { return lockfile.IsLocked(err) }

Try / catch

err := lockfile.Acquire(path)
if lockfile.IsLocked(err) {
    return retryWithBackoff() // LockFileEx failed immediately: another holder
}
if err != nil { return err }

Prevention

When it happens

Trigger: flockExclusive on a Windows lock file while another process holds the byte-range lock; surfaces through Acquire/FlockExclusiveNonBlock/AcquireSyncLock on Windows builds when contention occurs.

Common situations: Two bd processes on the same Windows machine targeting one workspace; an antivirus/editor or previous crashed-run descendant keeping a handle open; overlapping scheduled tasks.

Related errors


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