gitbutlerapp/gitbutler · error

Failed to bind to

Error message

Failed to bind to {url}: {e}

What it means

Public `run` in but-server binds a `tokio::net::TcpListener` to `host:port` (from config `bind_addr`, env `BUTLER_HOST`, or default 127.0.0.1, plus the chosen port) before serving the axum app. If the bind fails, the error is logged (with a special hint when the cause is `AddrInUse`) and re-raised as "Failed to bind to {url}: {e}", so the server never starts.

Solutions

  1. Stop the existing process holding the port (find it with `lsof -i :<port>` on Unix or `netstat -ano` on Windows) or use another port.
  2. Set the port/host via config `bind_addr` or the BUTLER_HOST env var to a valid local address.
  3. If binding a privileged port, run with the required permissions or pick a port >= 1024.
  4. Retry after transient bind failures, or add automatic port selection if the fixed port is expected to be free.

Example fix

// before
BUTLER_HOST=0.0.0.0 but-server --port 8585  // port taken by running instance
// after
lsof -ti :8585 | xargs kill   # or: BUTLER_HOST=127.0.0.1 but-server --port 8586
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: check the port is free before starting the server
ss -ltn | grep -q ':<port> ' && echo 'port in use' || echo 'port free'

Type guard

fn is_addr_in_use(err: &anyhow::Error) -> bool {
    err.chain().any(|c| {
        c.downcast_ref::<std::io::Error>()
            .map(|io| io.kind() == std::io::ErrorKind::AddrInUse)
            .unwrap_or(false)
    })
}

Try / catch

match but_server::run(config).await {
    Err(e) if e.to_string().contains("Failed to bind") && is_addr_in_use(&e) => {
        // stop the existing instance or restart with a different port
    }
    other => other?,
}

Prevention

When it happens

Trigger: Starting the server via `run` when `TcpListener::bind` fails: the port is already taken by another but-server/other process (AddrInUse), the port is privileged (<1024) without permission, the bind address is invalid or not local, or the port range is exhausted.

Common situations: Launching a second instance while one is already running (most common — the log explicitly hints at this); another service squatting on the port; typo'd BUTLER_HOST/bind_addr; docker/container networking where the address isn't assignable.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/e7aac9027fd4a442. Report an issue: GitHub.

Appendix: source

Thrown at crates/but-server/src/lib.rs:848

    let default_host = "127.0.0.1";
    let host_env = std::env::var("BUTLER_HOST").ok();
    let host = config
        .bind_addr
        .as_deref()
        .or(host_env.as_deref())
        .unwrap_or(default_host);
    let url = format!("{host}:{port}");
    let listener = match tokio::net::TcpListener::bind(&url).await {
        Ok(listener) => listener,
        Err(e) => {
            if e.kind() == std::io::ErrorKind::AddrInUse {
                tracing::error!(
                    "Failed to bind to {url}: {e}. Another instance of but-server may already be running on port {port}."
                );
            } else {
                tracing::error!("Failed to bind to {url}: {e}");
            }
            anyhow::bail!("Failed to bind to {url}: {e}");
        }
    };
    println!(
        "{} {}",
        "Local:".bold(),
        format!("http://localhost:{port}").cyan().underline()
    );
    let server = axum::serve(
        listener,
        app.into_make_service_with_connect_info::<SocketAddr>(),
    );

    tokio::select! {
        result = server => { result.unwrap(); }
        _ = tokio::signal::ctrl_c() => {
            // The settings file watcher (spawn_blocking with infinite loop) and
            // other background tasks prevent the tokio runtime from exiting
            // cleanly. It's safe to terminate immediately.

View on GitHub (pinned to 58e5313667)