gastownhall/beads · critical

bind %s: %w

Error message

bind %s: %w

What it means

The server binds its TCP listener via net.Listen and wraps any OS-level error with 'bind %s: %w', where %s is the configured address. This surfaces kernel/socket errors: address already in use, permission denied on privileged ports, or an invalid/unavailable address.

Source

Thrown at internal/httpapi/server.go:545

		sem:        make(chan struct{}, maxInflight),
		semTimeout: semAcquireTimeout,
		semWarn:    saturationWarn,

		closing:         make(chan struct{}),
		maxWatchStreams: maxWatchStreams,

		log:      log.New(cfg.Stderr, "bd serve: ", log.LstdFlags|log.LUTC),
		stdout:   cfg.Stdout,
		ctxBody:  contextResponse(cfg.Workspace, cfg.SchemaVersion, Capabilities()),
		hosts:    newHostPolicy(ip, cfg.AllowedHosts),
		auth:     cfg.Auth,
		idPrefix: prefix,
		maxConns: maxConns,
	}

	ln, err := net.Listen("tcp", cfg.Addr)
	if err != nil {
		return nil, fmt.Errorf("bind %s: %w", cfg.Addr, err)
	}
	s.listener = netutil.LimitListener(ln, s.maxConns)

	s.http = &http.Server{
		Handler:           s.handler(),
		ReadHeaderTimeout: readHeaderTimeout,
		ReadTimeout:       readTimeout,
		IdleTimeout:       idleTimeout,
		MaxHeaderBytes:    maxHeaderBytes,
		ErrorLog:          log.New(cfg.Stderr, "bd serve: http: ", log.LstdFlags|log.LUTC),
		ConnState:         s.connState,
	}
	// Tell the streams to wind up as soon as a drain starts. Without it a
	// graceful shutdown waits out the whole drain timeout on any open stream and
	// then reports itself forced, which is the one shutdown signal an operator
	// is meant to be able to trust.
	s.http.RegisterOnShutdown(s.closeStreams)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check what holds the port (lsof -i :PORT or ss -ltnp) and stop it, or pick a different port
  2. Use port 0 for an ephemeral port or a port above 1024 if lacking privileges
  3. Verify the IP in cfg.Addr is actually assigned to this host (ip addr); use 127.0.0.1 for local-only

Example fix

// before
Addr: "127.0.0.1:8080"  // already in use
// after
Addr: "127.0.0.1:8081"  // or "127.0.0.1:0" for ephemeral
Defensive patterns

Strategy: retry

Validate before calling

// check port availability before start
conn, err := net.Listen("tcp", addr)
if err == nil { conn.Close() }

Try / catch

srv, err := httpapi.Listen(cfg)
if err != nil {
    var oe *net.OpError
    if errors.As(err, &oe) && errors.Is(oe.Err, syscall.EADDRINUSE) {
        // pick another port or wait and retry
    }
}

Prevention

When it happens

Trigger: Calling Listen when another process already holds the port (EADDRINUSE), binding port <1024 without privileges (EACCES), or binding an IP not assigned to the machine (EADDRNOTAVAIL).

Common situations: Two instances of the server started simultaneously; a stale process still holding the port; running as non-root and trying port 80/443; container without the interface IP present.

Related errors


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