How HTTP Works

Debug guide · you're seeing

  • ERR_CONNECTION_TIMED_OUT
  • curl: (28) Connection timed out after 5000 milliseconds
  • curl: (28) Failed to connect to example.com port 443 after 5000 ms: Timeout was reached
  • ETIMEDOUT
  • connect ETIMEDOUT 203.0.113.10:443
  • TimeoutError: timed out
  • upstream timed out (110: Connection timed out) while connecting to upstream

ERR_CONNECTION_TIMED_OUT: Trace TCP Connection Failures

Fix ERR_CONNECTION_TIMED_OUT by tracing SYN packets, checking firewalls and stale DNS, testing IPv6, and separating backlog pressure from MTU stalls.

Reviewed 9 min readintermediate30 sourcesTry itMarkdown
On this page

TL;DR: Your client sent a connection request and never heard back. Run nc -vz -w 5 example.com 443 and curl --noproxy '*' -v --connect-timeout 5 --max-time 15 https://example.com/, and note the address after Trying and whether curl prints Connected to. If it never connects, capture SYN packets at both ends and check firewalls, DNS records and IPv6. If TCP connects and then TLS or the response stalls, you have a different problem, often path MTU.

What it means

Chromium defines ERR_CONNECTION_TIMED_OUT as a connection attempt that timed out. A TCP connection opens with SYN, SYN-ACK, ACK (RFC 9293). If the client keeps resending SYN and nothing comes back, packets are being silently dropped, the destination isn’t responding, or replies can’t find their way back. Which firewall or host is to blame is something you have to find out with captures.

It helps to separate three failures that look similar from the browser:

  • Timeout: the connection didn’t open before the deadline. A firewall rule that silently DROPs packets produces exactly this.
  • Refused: something actively said no, usually an RST because nothing listens on the port. A firewall set to reject does this too.
  • Reset: an RST killed the connection, often after it opened, during TLS or mid-response.

When a direct TCP connection fails, there’s no HTTP status at all, because HTTP never started. If a proxy is in the middle and its own upstream connection times out, you get an HTTP 504 instead.

Exact client and server messages

The messages below are examples based on the linked source code, not captured from real timeout runs. curl 8.7.1 has separate code paths for an overall timeout and a failed connection, plus the timeout description:

curl: (28) Connection timed out after 5000 milliseconds
curl: (28) Failed to connect to example.com port 443 after 5000 ms: Timeout was reached

The milliseconds depend on your deadline and how long curl actually waited. The second message appears when a single connection attempt hits curl’s timeout; an OS-level connect() timeout may be worded differently. Newer versions change the wording too: curl 8.22.0 prints the destination as example.com:443. Include curl --version in any bug report.

Node raises ETIMEDOUT when a connect or send got no response in time. Its connect error formatter produces messages like this one. Check err.syscall as well as err.code to see which operation timed out:

connect ETIMEDOUT 203.0.113.10:443

Python 3.14’s socket implementation raises TimeoutError: timed out. The message is the same whether connect, read or write expired, so check where in your code it was thrown.

In nginx’s error log on Linux, this line means nginx itself couldn’t connect to the upstream server:

upstream timed out (110: Connection timed out) while connecting to upstream

nginx’s upstream code logs both the timeout and what it was doing at the time. The 110: Connection timed out part comes from Linux’s errno definitions and the glibc message; other platforms use different numbers. If your log says while reading response header from upstream instead, the connection worked and the upstream was slow to answer. That’s covered in the nginx 504 guide.

Establish which phase failed

From the machine that sees the error, with your real hostname in place of example.com:

nc -vz -w 5 example.com 443
curl --noproxy '*' -v --connect-timeout 5 --max-time 15 https://example.com/

nc with OpenBSD-style options tests the TCP connection alone, with no HTTP involved. Flags vary between nc implementations, so check nc -h. --noproxy '*' makes curl ignore proxy environment variables so you test the direct route; if you normally go through a proxy, test that path as well.

Note that curl’s --connect-timeout covers more than the SYN: it includes DNS and the TCP, TLS or QUIC handshakes. --max-time caps the whole transfer. Then read the verbose output to see where it stopped:

  • No address, or name resolution failed: start with DNS debugging.
  • Trying and then nothing: TCP never connected. Work through the checks below.
  • Connected to, then the TLS handshake hangs: TCP is fine. Look at TLS and at packet-size problems.
  • TLS completes and the request goes out, but no response comes back: look at the application, any proxy, and the data path.

These commands test HTTPS over TCP. If the browser is using HTTP/3 over QUIC, they won’t reproduce its path; see HTTP/3 and QUIC for that case.

Fix it, in diagnostic order

1. A firewall drops the SYN or its reply

Run a capture on the client and on the server at the same time, then make one connection attempt. Swap in your real destination address:

# Linux; on macOS replace any with the active interface, such as en0
sudo tcpdump -ni any 'host 203.0.113.10 and tcp port 443'
# Narrow IPv4 filter: includes SYN and SYN-ACK, but excludes RST and data
sudo tcpdump -ni any 'tcp[tcpflags] & tcp-syn != 0'

The libpcap flag filter matches any packet with SYN set, which includes SYN-ACK. It only works for IPv4, though, and hides RST, ACK and data. For IPv6, or to see everything, use the host/port filter.

  • Client sends SYN, server never sees it: check the network path, cloud firewall, security group and the destination IP.
  • Server sees the SYN but never answers: check the host firewall, whether something is listening, and queue pressure.
  • Server sends SYN-ACK, client never gets it: check the return route and outbound filtering.

On a Linux server:

sudo ss -ltnp 'sport = :443'
sudo nft list ruleset
ip route get 198.51.100.24

Use your client’s real address there too. Then check the cloud firewall attached to that exact NIC or instance, because no local allow rule can override a drop that happens before the packet reaches the host. On AWS, security groups are stateful but network ACLs are stateless. So a subnet ACL has to allow both the inbound service port and the outbound replies to the client’s ephemeral port. AWS’s ACL examples explain why that port range depends on the client OS.

To allow a known client into a security group (the group ID and address are placeholders):

aws ec2 authorize-security-group-ingress \
  --group-id sg-0123456789abcdef0 \
  --protocol tcp --port 443 --cidr 198.51.100.24/32

Check first that an equivalent rule isn’t already there. The AWS CLI command only adds the ingress rule; the subnet ACL and route table are separate.

With nftables, a narrowly scoped rule in an existing inet filter input chain looks like this:

sudo nft insert rule inet filter input ip saddr 198.51.100.24 tcp dport 443 accept

Adjust the table, chain and source address to your setup, and save the rule through whatever manages your firewall config so it survives a reboot. insert puts it ahead of the chain’s existing rules, but other chains and upstream firewalls get their say too. Resist flushing the whole ruleset to test one port; you’ll open everything else as well.

2. DNS points at the wrong or retired machine

dig example.com. A
dig example.com. AAAA
dig @1.1.1.1 example.com. A
# Replace the address with the known-good endpoint
curl --noproxy '*' -v --connect-timeout 5 --max-time 15 \
  --resolve example.com:443:203.0.113.10 https://example.com/

Compare every returned address with your current load balancer or server inventory. --resolve points the hostname at an address you choose while keeping the hostname for TLS and HTTP. That’s a better test than https://IP/, which breaks certificate name checks and virtual-host routing.

If the override works but normal DNS hands out a retired address, fix the A/AAAA record and clear out stale hosts-file entries. Cached answers linger, so wait for them to expire before you shut down the old endpoint. If the hostname has several addresses, test each one. A single dead backend makes the failure look random, because it depends on which address the client picks.

3. IPv6 resolves but the path is broken

curl --noproxy '*' -4 -v --connect-timeout 5 --max-time 15 https://example.com/
curl --noproxy '*' -6 -v --connect-timeout 5 --max-time 15 https://example.com/
dig example.com. AAAA
# Linux server
sudo ss -ltnp 'sport = :443'
ip -6 route

If -4 works and -6 times out, the problem is specific to IPv6. Check that the AAAA record points at the right endpoint, that the server listens on IPv6, and that IPv6 routing and firewalls allow the connection and its replies. Then fix those, or remove the wrong AAAA record. Forcing -4 is a way to diagnose, not a reason to leave IPv6 off.

4. The server listens, but its queues fill under load

Linux keeps two queues: half-open handshakes, and finished connections waiting for the application to accept() them. The kernel documentation covers tcp_max_syn_backlog and the somaxconn cap on the listen backlog. When the accept queue fills up, the kernel drops new SYNs, and clients time out even though ss shows the port listening.

sudo ss -ltnp 'sport = :443'
ss -nt state syn-recv 'sport = :443'
nstat -az TcpExtListenOverflows TcpExtListenDrops
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog

Run these again while failures are happening. If ListenOverflows and ListenDrops are both climbing, the queue is overflowing (ListenDrops alone can rise for other reasons). The real cause is usually a blocked accept loop, saturated workers or too little capacity. A bigger queue helps absorb a burst, but the app still drains connections at the same speed.

If you’ve measured a burst that really needs more queue space, here’s an example Linux/nginx configuration asking for a backlog of 4096:

# Modify the existing listen directive; retain its other parameters
listen 443 ssl backlog=4096;
# /etc/sysctl.d/90-web-backlog.conf
net.core.somaxconn = 4096
sudo sysctl -p /etc/sysctl.d/90-web-backlog.conf
sudo nginx -t && sudo nginx -s reload

backlog sets what nginx requests from listen(), and the kernel’s somaxconn caps it. 4096 is an example, not a recommended value. Afterwards, confirm the config actually loaded and watch the counters.

TCP connected, then larger packets disappeared

In a classic PMTUD black hole, the small handshake packets get through, but larger packets are too big for some link on the path. The router that drops them is supposed to send an ICMP message back so the sender shrinks its packets. When a firewall blocks that ICMP, the sender never finds out. TCP connects fine, then TLS or the response stalls. That’s a different problem from SYNs with no reply.

# Linux: inspect established TCP details, including path MTU
ss -tin 'dport = :443'
sudo tcpdump -ni any '(host 203.0.113.10 and tcp port 443) or icmp or icmp6'

In the capture, look for large data segments being retransmitted over and over with no ICMP feedback, rather than handshake retries. The fix is to let IPv4 “fragmentation needed” and IPv6 “Packet Too Big” messages through your firewalls, and to set the right MTU on tunnels and interfaces for the path you measured. Setting a guessed MTU on every interface tends to create new problems.

As a test on the Linux machine that’s sending the large packets:

sudo sysctl -w net.ipv4.tcp_mtu_probing=1

This kernel setting turns on TCP MTU probing when the kernel detects an ICMP black hole. It only helps on the side sending the large packets; changing it on the client won’t fix the server’s sends. If the symptom goes away, still fix the blocked ICMP or the tunnel MTU. Probing is a workaround.

Trace the path without overreading it

A successful ping doesn’t tell you much here. Probe with TCP to the actual port instead:

# Linux traceroute's TCP mode
sudo traceroute -T -p 443 example.com
# Where tcptraceroute is installed
sudo tcptraceroute example.com 443
sudo mtr -T -P 443 -r -c 20 example.com

traceroute -T, tcptraceroute and mtr -T -P all send TCP SYNs to the port you choose. Flags differ between platforms, so check your installed tool’s manual. Read the results carefully: a hop that shows * hasn’t necessarily dropped your traffic, since many routers rate-limit or ignore probe replies while forwarding packets normally. What counts is whether the final destination answers, checked against your client and server captures.

Frequently asked questions

What does ERR_CONNECTION_TIMED_OUT mean?

The browser tried to open a connection and gave up waiting. For TCP, that usually means its SYN got no SYN-ACK back. Capture packets at both ends to find where they disappear: a firewall silently dropping them, a dead or overloaded host, or a broken return route.

How is a timeout different from connection refused?

Refused means something answered no, typically a TCP reset because nothing listens on that port. A timeout means nothing useful answered at all before the deadline. A reset on a connection that was already open is a third, separate failure.

Does curl exit code 28 prove that TCP could not connect?

No. Code 28 only says some deadline expired. curl’s connect timeout also covers DNS and the TLS handshake, and --max-time can run out while the response is downloading. Use -v output and a packet capture to see which phase stalled.

Why does curl -4 work while curl -6 times out?

The site has a working IPv4 path and a broken IPv6 one. Check the AAAA record, IPv6 routing, firewall rules and whether the server listens on IPv6. Fix IPv6 or remove the wrong AAAA record; forcing IPv4 is only for diagnosis.

Can an overloaded server time out while its port is listening?

Yes. When the accept queue is full, Linux drops new SYNs, so clients time out even though the port is open. Watch the listen queues and the ListenOverflows and ListenDrops counters during the failure, and fix slow accepting or missing capacity before raising backlog limits.

Can a path MTU problem cause a connection timeout?

Yes, but it looks different. In a PMTUD black hole the small handshake packets get through and larger ones vanish, so TCP connects and then TLS or the download stalls. curl may report a timeout in both cases, but a SYN with no reply is a different problem.

Sources

  1. MDN: connection management in HTTP/1.xdeveloper.mozilla.org
  2. RFC 9293: TCP establishment and reset processingrfc-editor.org
  3. RFC 2923: TCP path MTU black holesrfc-editor.org
  4. Chromium source: CONNECTION_TIMED_OUTgithub.com
  5. curl: timeout options, resolve override and exit codescurl.se
  6. curl 8.7.1 source: connection failure formattinggithub.com
  7. curl 8.7.1 source: timeout-state messagesgithub.com
  8. curl 8.7.1 source: timeout error descriptiongithub.com
  9. curl 8.22.0 source: destination formattinggithub.com
  10. Node.js: ETIMEDOUTnodejs.org
  11. Node.js source: connect error formattinggithub.com
  12. Python 3.14 source: socket timeout exceptiongithub.com
  13. nginx source: upstream connection and timeout logginggithub.com
  14. Linux source: generic ETIMEDOUT error numbergithub.com
  15. glibc source: Connection timed out messagegithub.com
  16. AWS: security groupsdocs.aws.amazon.com
  17. AWS: network ACLsdocs.aws.amazon.com
  18. AWS: custom ACLs and ephemeral return portsdocs.aws.amazon.com
  19. AWS CLI: authorize security-group ingressdocs.aws.amazon.com
  20. Netfilter: nftables rules and verdictsnetfilter.org
  21. Linux kernel: backlog limits and MTU probingkernel.org
  22. Linux kernel: ListenOverflows and ListenDropsdocs.kernel.org
  23. nginx: listen backlognginx.org
  24. iproute2: ss manualgithub.com
  25. iproute2 source: nstat optionsgithub.com
  26. OpenBSD: nc manualman.openbsd.org
  27. Linux: TCP tracerouteman7.org
  28. tcptraceroute: command manualgithub.com
  29. mtr source: TCP probes and port selectiongithub.com
  30. libpcap source: TCP flag filtersgithub.com

Keep going

Browse /search