Skip to content
rusthacks.dev
Tooling

Rust Error Connection Refused From Reqwest and Sockets

Connection refused means nothing was listening at that address and port. Check the bind address, localhost versus 127.0.0.1, containers and Docker networks, startup order and the wrong port, with commands for each.

4 min read
Summarize in AI
Flat illustration for the post “Rust Error Connection Refused From Reqwest and Sockets”, composed around errors.

The error looks like this, with reqwest's wrapper around the underlying I/O error:

Text
error sending request for url (http://localhost:3000/health)
Caused by:
    0: client error (Connect)
    1: tcp connect error: Connection refused (os error 111)

Error 111 on Linux (61 on macOS) is ECONNREFUSED. It is not an HTTP error, a TLS problem or a reqwest bug: the TCP handshake was rejected because no socket was listening at that IP and port. Everything below is about finding why. Commands are for Linux and macOS; code compiles on Rust 1.90 with edition 2024.

Step 1: is anything listening?

Shell
ss -ltnp | grep 3000          # Linux
lsof -nP -iTCP:3000 -sTCP:LISTEN   # macOS and Linux

No output means no process is listening on port 3000: the server is not running, crashed at startup, or listens on a different port. Check the server's logs and its configured port first. This is the cause in most cases, including the common one where the server exits immediately because of a configuration error and the client starts anyway.

Step 2: is it bound to the address you use?

rust
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    // reachable only from this machine
    let local = TcpListener::bind("127.0.0.1:3000").await?;
    // reachable from other machines and from outside a container
    let all = TcpListener::bind("0.0.0.0:3001").await?;
    println!("{} {}", local.local_addr()?, all.local_addr()?);
    Ok(())
}
Text
127.0.0.1:3000 0.0.0.0:3001

Binding 127.0.0.1 means "loopback only". A client on another host, or on the host connecting to a service in a container, is refused even though the process runs. In a container, bind 0.0.0.0 and control exposure with the published port (-p 127.0.0.1:3000:3000 keeps it private to the host). The ss output shows the bound address in the "Local Address" column: 127.0.0.1:3000 or *:3000.

Step 3: localhost can mean two addresses

Shell
getent hosts localhost         # Linux
dscacheutil -q host -a name localhost   # macOS

Many systems map localhost to both 127.0.0.1 and ::1. A server bound to 127.0.0.1 does not accept connections to ::1, and one bound to [::1] does not accept IPv4. Clients usually try both (Happy Eyeballs), but not all do, and a proxy or container setting can change the order. Use an explicit address (127.0.0.1) in the client and server, or bind [::] with dual-stack for both. In Rust, reqwest tries every resolved address; a refused error that mentions ::1 is the tell.

Step 4: Docker and Compose

YAML
services:
  api:
    build: .
    ports: ["3000:3000"]          # host access
  worker:
    build: ./worker
    environment:
      API_URL: http://api:3000    # service name, container port; NOT localhost
    depends_on:
      api:
        condition: service_healthy
  • Inside a container, localhost is the container itself. To reach the host from a container, use host.docker.internal (Docker Desktop, or with an extra_hosts entry on Linux).
  • Between Compose services, use the service name and the container port. The published host port is for the host only.
  • depends_on without a health condition only waits for the container to start, not for the server to listen.

Step 5: the server is not ready yet

rust
use std::time::Duration;

async fn wait_for(url: &str, attempts: u32) -> Result<(), reqwest::Error> {
    let client = reqwest::Client::new();
    let mut delay = Duration::from_millis(100);
    for attempt in 1..=attempts {
        match client.get(url).send().await {
            Ok(_) => return Ok(()),
            Err(e) if e.is_connect() && attempt < attempts => {
                tokio::time::sleep(delay).await;
                delay = (delay * 2).min(Duration::from_secs(5));
            }
            Err(e) => return Err(e),
        }
    }
    Ok(())
}

e.is_connect() identifies connection failures. In tests, wait for the server to be ready instead of sleeping a fixed time, or bind to port 0 and read the actual port, so the test knows the server is up (testing servers). In production, retry with backoff for dependencies that may restart (reqwest error handling).

Step 6: other causes

CauseCheck
Wrong port or environment variablePrint the URL the client uses; compare with ss
A proxy environment variable (HTTP_PROXY) sends traffic elsewhereenv | grep -i proxy; reqwest honors them; add NO_PROXY=localhost or .no_proxy()
Firewall rejecting rather than droppingiptables -L, cloud security groups (a REJECT rule gives refused)
Port conflict: another process holds it so yours failed to bindServer logs show "address already in use"
Kubernetes Service points at the wrong targetPortkubectl get endpoints, check the port mapping

A fast diagnosis

  1. curl -v http://HOST:PORT/ from the same place the program runs.
  2. If curl is refused too, fix the server or network (steps 1 to 4).
  3. If curl works and Rust fails, compare resolved addresses and proxy settings (step 3 and the table).

The related error "Too many open files" looks similar in logs but has a different cause (too many open files), as does a read timeout (tasks that never finish).

Also inWeb
Taggedreqwest
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.