Docker 'request canceled while waiting for connection': Fix
docker pull and docker login fail with net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) when the Docker daemon cannot complete a connection to Docker Hub. The error text is the same whether the real fault is a missing proxy, broken IPv6 routing, DNS flapping, or an egress firewall — so the fix is never "restart Docker" first, it is finding which layer dropped the connection.
The main Stack Overflow thread on this error has been open since December 2020 with 10,000+ views and zero accepted answers. Most advice in the wild tells you to add 8.8.8.8 to daemon.json — I have seen that fix fail repeatedly, because the daemon never consults that setting for its own registry connections. This post walks the diagnosis in the order that actually isolates the cause, with commands I verified against a live registry path on 2026-09-07.
TL;DR
- The message is a dial-phase timeout. "request canceled while waiting for connection" means the TCP connection never finished; "(Client.Timeout exceeded while awaiting headers)" is the client-side timeout wrapper. This is not a TLS error, not a credentials error, and not a disk error.
- Two endpoints must be reachable, not one:
registry-1.docker.io:443(manifest and blob traffic) andauth.docker.io:443(the bearer-token service Docker Hub uses on every authenticated pull). - Probe with
curlbefore touching any config. An HTTP 401 fromhttps://registry-1.docker.io/v2/means the network path is healthy — 401 is Docker Hub's correct answer to an unauthenticated request. daemon.json's"dns"key is a container setting. It does not change where the daemon resolvesregistry-1.docker.io. Fixing DNS at the host layer is what actually works.- Fix at the layer that connects: daemon proxy (systemd drop-in or
daemon.jsonproxies), host IPv6 routing, host DNS, or a registry mirror. Order matters — start with thecurlprobes below.
What "request canceled while waiting for connection" actually means
Docker's daemon talks to registries with an HTTP client that enforces timeouts. When you see this message, the client gave up while the connection was still being established — the TCP handshake (SYN) never completed, or no response headers arrived in time. You will meet it in two shapes. On docker login, the registry's base endpoint times out:
$ docker login
Username: myuser
Password:
Error response from daemon: Get https://registry-1.docker.io/v2/:
net/http: request canceled while waiting for connection
(Client.Timeout exceeded while awaiting headers)
On docker pull, the failure usually moves one hop further, to the token service. A pull first asks registry-1.docker.io for the manifest, gets a 401 with a WWW-Authenticate challenge, and then must fetch a bearer token from auth.docker.io before retrying. If that connection times out, the daemon reports the nested failure — the original Stack Overflow report shows exactly this:
$ docker pull cooldocker19/manas-simple-flask:latest
Error response from daemon: Head https://registry-1.docker.io/v2/
cooldocker19/manas-simple-flask/manifests/latest: Get
https://auth.docker.io/token?service=registry.docker.io&scope=repository
%3Acooldocker19%2Fmanas-simple-flask%3Apull: net/http: request canceled
while waiting for connection (Client.Timeout exceeded while awaiting
headers)
That second shape explains a confusing detail people report: docker login succeeds (credentials are checked against the cached token flow) but docker pull still fails — the pull is the operation that forces a fresh token fetch. It also explains why "try again later" sometimes appears to work: the failure is intermittent because the broken layer is intermittent (a flapping resolver, a congested VPN).
Know your siblings so you do not chase the wrong fix:
net/http: TLS handshake timeout— the TCP connection completed but TLS did not. Proxy TLS interception and SNI filtering live here, not in this article's territory.EOFmid-response — the connection was established and then dropped. That family is covered in our curl (52) empty reply write-up.request canceled while waiting for connection— the connection itself never came up. DNS, routing, proxy, and firewall are the suspects.
Diagnose the failing layer before you change anything
Run these probes from the same network the daemon uses. On a Linux host with a systemd-managed daemon, that is the host itself. On Docker Desktop, the daemon runs inside a VM, so treat the probes as a test of the host path first, then re-check the Desktop-specific layer (more in the Desktop section below). A 401 response is a pass: it proves the TCP+TLS path to the registry works, because Docker Hub answers unauthenticated /v2/ requests with 401.
# Probe 1 — IPv4 path to the registry. 401 = healthy.
curl -4 -sS -o /dev/null -w 'v4 registry: %{http_code} in %{time_total}s\n' \
--connect-timeout 5 https://registry-1.docker.io/v2/
# Probe 2 — IPv6 path. Fails fast on hosts without IPv6 routing.
curl -6 -sS -o /dev/null -w 'v6 registry: %{http_code} in %{time_total}s\n' \
--connect-timeout 5 https://registry-1.docker.io/v2/
# Probe 3 — the bearer-token endpoint used on authenticated pulls.
curl -sS -o /dev/null -w 'auth: %{http_code} in %{time_total}s\n' --connect-timeout 5 \
'https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/hello-world:pull'
# Probe 4 — does resolution flap? Run it a few times and count.
for i in $(seq 1 10); do getent ahosts registry-1.docker.io | head -1; sleep 1; done \
| sort | uniq -c
I ran this exact set on a test host on 2026-09-07. Probe 1 returned 401 in 0.26s, Probe 3 returned 200 in 0.12s, and Probe 2 died in about two milliseconds with curl: (7) Failed to connect … Could not connect to server. That host has IPv4 routes but no IPv6 route, while the registry publishes both A and AAAA records — a textbook IPv6 blackhole, and the daemon's error message on that host is precisely the timeout this article is about.
| Probe result | Layer at fault | Jump to |
|---|---|---|
| Probe 1 fails (no 401) | IPv4 egress: firewall, offline, or Docker Hub blocked on the network | Fix 4 (mirror) after checking basic connectivity |
| Probe 2 fails, Probe 1 passes | IPv6 records with no working IPv6 route | Fix 2 |
| Probes pass from the shell, Docker still times out | Daemon environment: proxy, VM layer (Desktop), or MTU | Fix 1, Fix 5, Desktop section |
| Probe 4 shows changing or failing answers | DNS at the host layer | Fix 3 |
⚠️ Do not use ping as your only reachability test. Many cloud and VPS providers block ICMP at the security layer while HTTPS works normally — on the test host above, ping -M do -s 1472 to the registry IP returned 100% packet loss in the same minute that Probe 1 returned 401 in 0.26 s. TCP probes with curl test the path your daemon actually uses.
Fix 1 — the proxy that never reached the daemon
The classic corporate failure: HTTP_PROXY and HTTPS_PROXY are exported in your shell, curl works through the proxy, and the daemon still times out. The daemon is a systemd service — it does not inherit your interactive shell's environment, and when it was started without proxy variables it dials Docker Hub directly into a firewall that only allows the proxy out.
Check what the daemon actually sees before configuring anything:
# Environment of the running daemon process (systemd hosts):
sudo systemctl show docker --property=Environment
# Or read the process environment directly (read must run as root):
sudo sh -c 'tr "\0" "\n" < /proc/$(pgrep -x dockerd)/environ' | grep -i proxy
If the daemon shows no proxy variables, configure them. The Docker daemon proxy documentation gives two supported mechanisms. The systemd drop-in is the most robust on distros with systemd:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf > /dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,.local,.internal"
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
Alternatively, set the proxy in /etc/docker/daemon.json (daemon config takes precedence over environment variables):
{
"proxies": {
"http-proxy": "http://proxy.example.com:3128",
"https-proxy": "http://proxy.example.com:3128",
"no-proxy": "localhost,127.0.0.1,.internal"
}
}
That block is the daemon-side file — the hyphenated http-proxy/https-proxy/no-proxy keys under proxies are the format the daemon reads in /etc/docker/daemon.json, per the daemon proxy documentation above. Do not confuse it with the Docker CLI's own proxy config, which lives in ~/.docker/config.json and uses different camelCase keys per command ({"proxies": {"default": {"httpProxy": …, "httpsProxy": …, "noProxy": …}}}). The error in this article is daemon-side — the message starts with Error response from daemon — so the daemon file is the one that matters for it.
💡 Keep NO_PROXY populated. If you also pull from a self-hosted registry on your LAN, listing it in no-proxy stops the daemon from routing an internal address through the corporate proxy — which otherwise produces a different but equally confusing timeout. Docker Desktop ignores daemon.json proxies entirely; on Desktop you configure proxies in Settings → Resources → Proxies, per the same documentation.
Fix 2 — IPv6 records with no IPv6 route
Docker Hub publishes AAAA records for registry-1.docker.io alongside its A records. On a host with broken or absent IPv6 routing — a common state on cloud VMs, VPN clients, and dual-stack LANs where the provider never actually routes IPv6 — the daemon can spend its whole connect budget walking toward an unreachable IPv6 address. The result is exactly request canceled while waiting for connection, because from the client's perspective the connection never completes.
The Probe 2 result from the test host is the fingerprint: IPv6 dial fails fast when there is no route at all, but on networks where the IPv6 path silently drops packets, the same probe hangs until --connect-timeout fires. Both states produce the Docker error, and both mean the same thing: do not let the daemon's first dial go to IPv6 on a host that cannot deliver it.
The clean fix is to give the host working IPv6 routing (enable it on the interface, or fix the VPN's IPv6 handoff). Where the provider genuinely has no IPv6, disabling IPv6 on the host's external interface is the pragmatic remedy — not on the loopback, and not inside containers:
# Verify the blackhole first: this must fail while Probe 1 passes.
curl -6 -sS --connect-timeout 5 https://registry-1.docker.io/v2/ || echo "no IPv6 path"
# Hosts where the provider never routes IPv6 (cloud VMs without IPv6):
sudo sysctl -w net.ipv6.conf.eth0.disable_ipv6=1
# Make it permanent:
echo 'net.ipv6.conf.eth0.disable_ipv6=1' | sudo tee /etc/sysctl.d/90-disable-ipv6.conf
⚠️ Disabling IPv6 is a host-level decision, not a Docker one — if other services on the machine legitimately use IPv6, fix the route instead. And keep the change off Docker's bridge networks; container IPv6 is configured separately and is not what the daemon uses to reach the registry.
Fix 3 — DNS at the wrong layer
The daemon resolves registry-1.docker.io with the host it runs on. The "dns" key in daemon.json only configures the resolver that Docker hands to containers on its networks — the daemon's own outbound registry connections never consult it. That is why the "add 8.8.8.8 to daemon.json" advice fails so often, and it is exactly what the open Stack Overflow question documents: the asker added public DNS to the Docker configuration, saw no change, and only then discovered via dig that resolution itself was intermittently failing.
When resolution flaps, the pull error appears intermittently — minutes apart, the same command succeeds then times out. Probe 4 catches this. If the output shows multiple different IPs across ten runs, or empty lines where lookup failed, the resolver is your problem:
# What does the host resolve right now?
getent ahosts registry-1.docker.io | head -2
getent ahosts auth.docker.io | head -2
# systemd-resolved in use? Check what it hands out.
resolvectl status 2>/dev/null | grep -A2 'DNS Servers' | head -6
Fix DNS where the daemon actually resolves. On a Linux host that is /etc/resolv.conf (or the systemd-resolved configuration behind it); on Docker Desktop it is the host's resolver, which the Desktop VM forwards to. If your corporate resolver returns poisoned or filtered answers for Docker Hub domains, point the host at a working resolver — 1.1.1.1 or 8.8.8.8 — and confirm with getent afterwards. Restarting Docker is not required for a host-level DNS change to take effect; the next pull uses the new answer.
Fix 4 — egress blocked or throttled: add a registry mirror
Some networks filter or throttle Docker Hub by policy or geography. If Probe 1 fails from the host itself and the network genuinely allows general HTTPS traffic, the pragmatic fix is a registry mirror: the daemon tries your configured mirror first and falls back to Docker Hub. The registry-mirrors key is part of the official daemon configuration reference ("Specifies a list of registry mirrors"):
{
"registry-mirrors": ["https://mirror.example-region.example"]
}
sudo systemctl restart docker
docker pull hello-world # smoke test — no credentials needed
Use a mirror you have a reason to trust: your cloud provider's Docker Hub cache, or a registry:2 instance you run yourself on a network with better egress. Public community mirrors come and go and some have served malicious images in the past; a mirror sees every image you pull, so treat it as infrastructure, not as a random URL from a forum. After switching, docker pull hello-world is the fastest end-to-end verification that the daemon's registry client, auth flow, and image store all work again.
Fix 5 — MTU mismatch on VPNs and overlays
When the registry path crosses a VPN or an overlay with a maximum packet size below 1500 bytes, large exchanges stall: the TCP handshake and small requests squeak through, then bigger segments vanish into a path-MTU blackhole and the client eventually reports the same waiting-for-connection timeout. Because the daemon's pulls originate in the host network namespace, the MTU that matters for docker pull is the MTU of the host's outbound interface — the one carrying the tunnel — not Docker's bridge MTU.
# Find the working packet size (payload + 28 bytes of IP/ICMP header).
# On hosts where ICMP is allowed:
ping -M do -s 1472 <registry-ip> # 1500 total — fails over a 1400 tunnel
ping -M do -s 1372 <registry-ip> # 1400 total — passes
# Lower the outbound interface to the value the probe found (example: 1400).
sudo ip link set dev eth0 mtu 1400
ip link show eth0 # verify: mtu 1400
Persist the change with your distribution's network tooling — netplan (/etc/netplan/*.yaml), NetworkManager, or systemd-networkd — or a VPN client will reset it on the next reconnect. If the traffic that times out originates inside a container on a Docker network that crosses the same tunnel, that container traffic follows Docker's network MTU instead, and you lower it at the Docker layer ("mtu": 1400 in daemon.json for the default bridge, or the com.docker.network.driver.mtu option on a custom network). Daemon-side pulls themselves are host traffic and follow the host interface MTU.
⚠️ If ping shows 100% loss even at small sizes, ICMP is blocked on that path (as it was on my test host) — fall back to the curl probes and adjust MTU based on what your VPN documents, or test with a real large HTTPS download. The well-known moby issue 22635 tracks the VPN/MTU variant of this exact timeout for anyone who wants the full history.
Docker Desktop and WSL2 specifics
On Docker Desktop (macOS and Windows), the engine runs in a VM, so three things from this article move one layer down. First, proxies: Desktop ignores daemon.json proxies, so configure them in the Desktop settings UI. Second, DNS: the VM forwards to your host resolver, so fix the host's DNS — and if you are on a corporate VPN, check the VPN's DNS, not the Desktop VM. Third, the VM's network stack inherits your host's VPN MTU problems; if pulls fail only while the VPN is connected, that is the same path-MTU story as Fix 5, applied to the tunnel your whole machine is on. If you run the engine natively inside a WSL2 distro instead of Desktop, the Linux fixes above apply inside the distro — and the networking differences between the two WSL2 setups are covered in our WSL2 network_mode: host post.
Prevention checklist
- Probe before you configure. The four
curl/getentprobes take ten seconds and tell you which layer to fix; every config change after a blind guess makes the next diagnosis harder. - Keep proxy configuration in exactly one place per platform — systemd drop-in or
daemon.jsonon Linux, Desktop settings on macOS/Windows — and keepNO_PROXYcurrent for internal registries. - Do not put public DNS in
daemon.jsonto fix pulls. It is a container setting; fix the host resolver instead and verify withgetent. - Document your network's real MTU once you have measured it; the number belongs in your host/VM provisioning notes so the next VPN setup starts from the known value.
- Smoke-test after any registry-path change with
docker pull hello-world— no credentials, small download, exercises manifest fetch, token auth, and blob download in one command.
If the error returns after you applied the fix your probes pointed to, re-run the probes rather than restarting the daemon — the failure is intermittent when its cause is, and the probes will show you which endpoint flaps. When the problem is instead that containers fail to start or ports will not publish, that is the separate daemon-side failure we covered in our docker driver failed programming external connectivity post.