david kimani
Work with me

curl could reach it, Node could not

A deploy check reported a total outage against a site that was serving fine. The difference was 250 milliseconds of Happy Eyeballs, and it cost me an hour twice.

The report that was wrong

A post-deploy smoke script came back with twenty failures, all identical:

text
GET /          TypeError: fetch failed
GET /services  TypeError: fetch failed
GET /blog      TypeError: fetch failed

Twenty out of twenty-one checks. The site was down.

Except it was not. curl returned 200 for every one of those URLs, from the same machine, seconds apart.

Two address families, one of them a black hole

The host resolved to both an IPv4 address and an IPv6 address. The IPv4 worked. The IPv6 accepted nothing.

curl implements Happy Eyeballs: try both families, keep whichever answers first. It reached the site on the first attempt every time.

Node implements Happy Eyeballs too. autoSelectFamily has been on by default since Node 20, and I checked that it was on. So why did it hang?

The answer is the timeout sitting next to it. autoSelectFamilyAttemptTimeout defaults to 250 milliseconds. That is how long Node gives the first address before it moves to the next one. If the IPv4 connection has not completed inside that window, Node starts the IPv6 attempt as well, the IPv6 attempt goes nowhere, and the request sits there until the whole thing times out.

250ms is not much for a TLS handshake to a shared host on another continent.

The fix is two lines

js
import net from 'node:net'

net.setDefaultAutoSelectFamily(true)
net.setDefaultAutoSelectFamilyAttemptTimeout(750)

Long enough for a real connection to win the race, short enough that a genuinely dead address still fails quickly.

Why it is worth knowing

The failure looks like a total outage from inside your own tooling and like perfect health from everywhere else. I nearly went looking at DNS, then at the web server, then at the firewall.

The thing to try first is the same request through curl. If curl succeeds and Node does not, you are not looking at a network problem. You are looking at a difference between two clients, and the list of those differences is short.

I hit this twice in one week: once in a deploy preflight and once in the smoke test that runs after it. Both were checks whose entire job is to report whether something is healthy, and both reported the opposite of the truth. A check that cries wolf gets ignored, and an ignored check is worse than no check at all.

Share
XLinkedIn

Enjoyed this post? Subscribe for more. No noise, unsubscribe anytime.

Related posts