Docker net/http request canceled on a registry call
A docker net/http request canceled failure on a pull or push is the Go HTTP client in the daemon giving up on a deadline, and the middle of the sentence tells you which phase it died in. "While waiting for connection" means it never obtained one, so the registry had not yet been asked anything and the problem is between the runner and the host.

What this error means
A pull or a push fails after a noticeable pause, usually around fifteen seconds, and the URL in the message ends in the registry API root rather than at a blob or a manifest. It is intermittent: the same job passes on a re-run with nothing changed. The message is longer than it looks because three pieces of the Go standard library contribute to it, and the whole sentence appears in none of them. No run is recorded here, and the line below is set out as those pieces format it.
--- the ping client giving up before it had a connection
Error response from daemon: Get "https://registry.example.net/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)The sentence is three strings, and each one narrows the diagnosis
The Go HTTP transport keeps two similar errors apart on purpose. One reads "net/http: request canceled" and is the general cancellation. The other reads "net/http: request canceled while waiting for connection", and the transport substitutes it specifically when the request was still inside the step that obtains a connection, whether from the idle pool or by dialing. So the longer wording is a statement about phase.
The suffix comes from somewhere else. The client appends "(Client.Timeout exceeded while awaiting headers)" when its own deadline expired during the send, which is what tells you a deadline rather than a cancellation is responsible. Finally the daemon wraps the URL, and the Docker API client prefixes the daemon error.
Pasting the whole line into a code search will find nothing, because it is assembled at runtime from parts that live in different files. That is not evidence of anything being invented; it is what a composite message looks like. Search a distinctive fragment of one part instead.
| Fragment | Where it is formatted | What it narrows down |
|---|---|---|
net/http: request canceled while waiting for connection | The Go HTTP transport, when the deadline lands during connection acquisition | The registry was never asked anything |
(Client.Timeout exceeded while awaiting headers) | The Go HTTP client, appended when its own deadline expired | A deadline, not a context cancellation or a reset |
Get "https://.../v2/" | The Go URL error wrapper, carrying method and URL | The registry API root, so this was the initial ping |
Error response from daemon: | The Docker API client | The daemon, not the CLI, made the request |
Common causes
A transient network fault between the runner and the registry
The common case, and the reason the job passes on a re-run with nothing changed. A brief loss of connectivity, a congested link or a registry edge that stops accepting connections for a few seconds is enough to outlast a fifteen second deadline. Nothing about the image, the tag or the credentials is involved.
Name resolution that stalls rather than fails
A resolver that answers slowly is worse than one that answers wrongly, because a refusal returns immediately and a stall consumes the whole budget. The message is identical either way, since the connection was never obtained. Timing a lookup separately is what separates this from the rest.
A proxy or firewall that drops the connection silently
A blocked route that sends a refusal fails fast and says so. One that discards packets leaves the client waiting until its deadline, which produces exactly this wording. In our experience this is the version that repeats on every run rather than intermittently, which is the tell that it is policy rather than weather.
Proxy variables the daemon never sees
Setting proxy variables in a workflow step configures that step. The daemon is a separate process started before the job, so it uses its own environment, and a registry reachable only through a proxy will time out for the daemon while curl in the same job succeeds. That difference is a diagnosis in itself.
How to fix it
Retry with backoff, because this one is usually weather
Unlike most registry failures, this is worth retrying. Keep it bounded and print the attempt, so a route problem shows up as three identical failures rather than hiding inside a loop that eventually gives up.
for i in 1 2 3; do
docker pull registry.example.net/acme/api:1.4.2 && exit 0
echo "attempt $i timed out, waiting $((i * 10))s"
sleep $((i * 10))
done
echo "::error::three attempts timed out, this is not transient"
exit 1Configure the proxy for the daemon, not just for the step
- Put the proxy settings where the daemon reads them rather than in the job environment.
- Include the registry host in the no-proxy list if it is reachable directly.
- Restart the daemon and wait for it to answer before the first pull.
- name: Give the daemon its own proxy configuration
run: |
sudo mkdir -p /etc/systemd/system/docker.service.d
printf '[Service]\nEnvironment="HTTPS_PROXY=http://proxy.example.net:3128"\nEnvironment="NO_PROXY=localhost,127.0.0.1"\n' \
| sudo tee /etc/systemd/system/docker.service.d/proxy.conf
sudo systemctl daemon-reload && sudo systemctl restart docker
timeout 60 bash -c 'until docker info >/dev/null 2>&1; do sleep 2; done'Pull base images through a mirror you control
A registry mirror moves the dependency from a public host to one inside your own network, which removes both the distance and the shared load. It also means a public registry having a bad minute stops being a reason your pipeline fails.
{ "registry-mirrors": ["https://mirror.example.net"] }Fail fast when it is not transient
Three identical timeouts are not weather. Have the job say so rather than retrying into a red run twenty minutes later, and print the connection timing at that point so the log carries the evidence for whoever reads it next.
curl -sS -o /dev/null -m 20 -w "connect=%{time_connect}s\n" https://registry.example.net/v2/ \
|| echo "::error::the runner cannot reach the registry at all"Where the fifteen seconds comes from
The daemon builds a dedicated HTTP client for the registry ping and for the authentication exchange, and both are constructed with a timeout of fifteen seconds. That value is written into the source rather than read from a flag or a configuration file, so there is nothing to tune. It was fifteen seconds in the sources for Docker 27 and 28 and on the current main branch, each read on 2026-09-20.
That explains the shape of the failure. A URL ending at the registry API root, a pause of roughly fifteen seconds, and a complaint about never getting a connection together mean the ping could not reach the host in the time allowed. It is not a slow image, a big layer or a busy registry backend, because none of those had been consulted yet.
Tell a slow path from a blocked one
The useful question is whether the runner can open a connection to that host at all, and how long it takes when it can. Measure it directly rather than inferring from the Docker failure, because Docker will keep telling you the same thing.
A connect that succeeds quickly when tested, while pulls keep timing out, points at load or at a proxy that handles the two differently. A connect that hangs for the full timeout is a route or firewall answer and the fix is outside Docker.
- name: Time the connection the daemon is failing to make
run: |
getent hosts registry.example.net || echo "resolution failed"
curl -sS -o /dev/null -m 20 \
-w "connect=%{time_connect}s tls=%{time_appconnect}s total=%{time_total}s\n" \
https://registry.example.net/v2/ || echo "curl could not complete either"Why no recorded run backs this page
The condition is a network failure lasting longer than fifteen seconds between a runner and a registry, and we cannot schedule one. What we could do is manufacture it, by pointing the daemon at a black-holed address until the deadline expires, and that would produce a real log of a failure nobody actually has. It would look like evidence and would be a recording of our own firewall rule.
This page does not need it. The two Go strings, the substitution rule that chooses between them, and the fifteen second client are all readable in source, and together they say more about a reader log than a screenshot of ours would. A retry is also the right first move here regardless of what a recorded run showed, which is unusual on this cluster and worth saying plainly.
How to prevent it
- Retry registry calls with backoff, and cap the retries so a route problem is visible.
- Configure proxies for the daemon rather than for the shell step.
- Mirror the base images your pipeline pulls on every run.
- Log connection timing next to any registry failure so the next one has a baseline.
Frequently asked questions
What does while waiting for connection actually mean?
Can I raise the timeout?
Is this the same as a TLS handshake timeout?
Why does curl work in the same job when Docker times out?
Related guides
References
- Go: the transport error used when a request is still waiting for a connection
- Go: the client that appends the Client.Timeout suffix
- moby: the registry ping and authentication client, with its timeout
- Docker docs: configuring the daemon and registry mirrors
- Docker documentation
- Docker build cache
- GitHub Actions documentation