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.

The error looks like this, with reqwest's wrapper around the underlying I/O error:
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?
ss -ltnp | grep 3000 # Linux
lsof -nP -iTCP:3000 -sTCP:LISTEN # macOS and LinuxNo 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?
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(())
}127.0.0.1:3000 0.0.0.0:3001Binding 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
getent hosts localhost # Linux
dscacheutil -q host -a name localhost # macOSMany 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
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,
localhostis the container itself. To reach the host from a container, usehost.docker.internal(Docker Desktop, or with anextra_hostsentry on Linux). - Between Compose services, use the service name and the container port. The published host port is for the host only.
depends_onwithout a health condition only waits for the container to start, not for the server to listen.
Step 5: the server is not ready yet
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
| Cause | Check |
|---|---|
| Wrong port or environment variable | Print the URL the client uses; compare with ss |
A proxy environment variable (HTTP_PROXY) sends traffic elsewhere | env | grep -i proxy; reqwest honors them; add NO_PROXY=localhost or .no_proxy() |
| Firewall rejecting rather than dropping | iptables -L, cloud security groups (a REJECT rule gives refused) |
| Port conflict: another process holds it so yours failed to bind | Server logs show "address already in use" |
Kubernetes Service points at the wrong targetPort | kubectl get endpoints, check the port mapping |
A fast diagnosis
curl -v http://HOST:PORT/from the same place the program runs.- If curl is refused too, fix the server or network (steps 1 to 4).
- 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).


