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

  1. Retry the operation shortly after; the winning process publishes its endpoint and the next attempt will adopt the existing proxy instead of spawning.
  2. Check for unintended concurrency against the same rootDir (parallel scripts, cron overlap, duplicate daemons) and serialize them.
  3. Inspect the lock file under the workspace root to confirm who holds proxy.lock and wait for that holder to finish.
  4. 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

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


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