gastownhall/beads · warning
proxy child lost the proxy.lock spawn race for %s: %w
Error message
proxy child lost the proxy.lock spawn race for %s: %w
What it means
This error is returned by spawnAndHandoff when the freshly spawned db-proxy child process exits with the special LockHeldExitCode. That exit code means another process already held the proxy.lock, so the child lost the spawn race and deliberately exited instead of starting a second proxy. It is not a listen or backend failure; the library surfaces it distinctly so callers know another concurrent starter won.
Source
Thrown at internal/storage/dbproxy/proxy/endpoint.go:378
if discovered.status == adoptionIOErr {
return Endpoint{}, fmt.Errorf("discover spawned proxy: %w", discovered.err)
}
select {
case childErr := <-child.done:
if interrupted, ierr := stopEpochChanged(rootDir, stopEpoch); ierr != nil {
return Endpoint{}, ierr
} else if interrupted {
return Endpoint{}, fmt.Errorf("%w for %s", errStartInterrupted, rootDir)
}
if childErr == nil {
childErr = errors.New("child exited without reporting an error")
}
// A LockHeldExitCode exit is a lost spawn race, not a listen
// failure; any other exit gets the child's log path so the real
// error (listen, backend start, ...) is findable.
var exitErr *exec.ExitError
if errors.As(childErr, &exitErr) && exitErr.ExitCode() == LockHeldExitCode {
return Endpoint{}, fmt.Errorf(
"proxy child lost the proxy.lock spawn race for %s: %w",
rootDir, childErr,
)
}
if opts.Port != 0 {
return Endpoint{}, fmt.Errorf(
"proxy child exited before becoming ready on explicitly configured port %d (see %s): %w",
opts.Port, opts.LogFilePath, childErr,
)
}
return Endpoint{}, fmt.Errorf(
"proxy child exited before publishing its OS-assigned port (see %s): %w",
opts.LogFilePath, childErr,
)
case <-hard.C:
if err := killSpawnedChild(child); err != nil {
return Endpoint{}, fmt.Errorf("hard timeout waiting for proxy on %s; safe child kill failed: %w", describeSpawnPort(opts.Port), err)
}View on GitHub (pinned to 71377f2769)
Solutions
- Retry the operation shortly after; the winning process publishes its endpoint and the next attempt will adopt the existing proxy instead of spawning.
- Check for unintended concurrency against the same rootDir (parallel scripts, cron overlap, duplicate daemons) and serialize them.
- Inspect the lock file under the workspace root to confirm who holds proxy.lock and wait for that holder to finish.
- If the lock is genuinely stale (holder process is dead), remove the stale lock and retry the start.
Example fix
// before: starting proxy inside parallel fan-out causes spawn-race errors
results := startProxiesInParallel(workspaces)
// after: serialize per workspace, then retry once on spawn-race loss
for _, ws := range workspaces {
ep, err := GetCreateDatabaseProxyServerEndpoint(ws, opts)
if errors.Is(err, os.ErrPermission) || strings.Contains(err.Error(), "lost the proxy.lock spawn race") {
time.Sleep(500 * time.Millisecond)
ep, err = GetCreateDatabaseProxyServerEndpoint(ws, opts) // adopts winner's proxy
}
} Defensive patterns
Strategy: retry
Validate before calling
// Best effort: detect an already-running proxy before spawning to avoid racing.
if ep, err := DiscoverDatabaseProxyServerEndpoint(rootDir); err == nil {
return ep // someone already started it
} Try / catch
ep, err := GetCreateDatabaseProxyServerEndpoint(rootDir, opts)
if err != nil && strings.Contains(err.Error(), "lost the proxy.lock spawn race") {
time.Sleep(500 * time.Millisecond)
ep, err = GetCreateDatabaseProxyServerEndpoint(rootDir, opts) // adopt winner's proxy
} Prevention
- Avoid launching multiple concurrent cold starts against the same workspace root
- Serialize per-workspace startup with your own application-level mutex
- Prefer discovering an existing proxy before spawning a new one
- Monitor for overlapping cron/CI jobs sharing a workspace
When it happens
Trigger: Two or more processes call GetCreateDatabaseProxyServerEndpoint (via spawnAndHandoff) for the same rootDir nearly simultaneously; the loser's child detects the lock is held and exits with LockHeldExitCode, which the parent re-reports wrapped with the child's exit error.
Common situations: Running multiple bd commands or agent sessions against the same workspace concurrently; a race between a foreground command and a background daemon both cold-starting the proxy; CI jobs sharing a checked-out workspace directory.
Related errors
- ErrLockHeld
- dolt sql-server exited before listener became ready
- server: ExternalDoltServer.Start: server already started
- acquiring lock (blocking): %w
- acquiring lock (non-blocking): %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/d2a512f8e4b083bb.
Report an issue: GitHub.