gastownhall/beads · info

errIdleTimeout

errIdleTimeout

Error message

idle timeout reached

What it means

errIdleTimeout is returned by the proxy's idleWatcher when no client activity occurs for the configured idle timeout. The proxy then shuts itself down (including the backend) as a resource-saving measure. It is an internal lifecycle sentinel, not an operational fault.

Source

Thrown at internal/storage/dbproxy/proxy/server.go:92

const LockHeldExitCode = 75

// ErrLockHeld is returned from ListenAndServe when another proxy already
// holds proxy.lock for the same rootDir. It is a normal "lost the race"
// outcome, not a failure: callers spawned as children should map it to
// LockHeldExitCode and exit cleanly.
var ErrLockHeld = errors.New("proxy lock held by another proxy on this rootDir")

const (
	serverReadyTimeout     = 30 * time.Second
	readyDialTimeout       = 2 * time.Second
	readyInitialBackoff    = 50 * time.Millisecond
	readyMaxBackoff        = 1 * time.Second
	idleWatcherMinInterval = 1 * time.Second
	backendStopTimeout     = 5 * time.Minute
	tcpKeepAlivePeriod     = 30 * time.Second
)

var errIdleTimeout = errors.New("idle timeout reached")

func NewProxyServer(opts ProxyOpts) *proxyServer {
	return &proxyServer{
		rootDir:     opts.RootDir,
		port:        opts.Port,
		idleTimeout: opts.IdleTimeout,
		server:      opts.Server,
		stats:       opts.Stats,
		stopEpoch:   opts.StopEpoch,
	}
}

func (p *proxyServer) tracef(format string, args ...any) {
	p.logger.Printf(format, args...)
}

func (p *proxyServer) ListenAndServe(parentCtx context.Context) error {
	lock, err := util.TryLock(filepath.Join(p.rootDir, LockFileName))

View on GitHub (pinned to 71377f2769)

Solutions

  1. Increase IdleTimeout in ProxyOpts to cover expected quiet periods
  2. Set a long/very large idle timeout for daemons that must stay up
  3. Reconnect on demand: the proxy is designed to restart when a client needs it

Example fix

// before
srv := proxy.NewProxyServer(proxy.ProxyOpts{RootDir: root, IdleTimeout: 30 * time.Second})
// after
srv := proxy.NewProxyServer(proxy.ProxyOpts{RootDir: root, IdleTimeout: 30 * time.Minute})
Defensive patterns

Strategy: try-catch

Validate before calling

if opts.IdleTimeout != 0 && opts.IdleTimeout < time.Minute {
    opts.IdleTimeout = time.Minute // floor for interactive workloads
}

Try / catch

if err := srv.ListenAndServe(ctx); err != nil {
    if errors.Is(err, errIdleTimeout) || strings.Contains(err.Error(), "idle timeout") {
        return nil // expected self-shutdown; clients will restart on demand
    }
    return err
}

Prevention

When it happens

Trigger: Running ListenAndServe with a short ProxyOpts.IdleTimeout and letting no clients connect/dial for that duration; long-running sessions with a quiet period exceeding the timeout.

Common situations: Long think-time pauses in agent workflows; background daemon stopped unexpectedly by the idle watcher; misconfigured IdleTimeout (e.g. seconds instead of minutes).

Understand the failure class

Related errors


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