/ writing / debug

The IPv6 ghost in the sandbox

2026-07-28 · cynix · 4 min read

An actor worked locally. It worked in CI. It failed in Apify's cloud sandbox with ECONNREFUSED on an HTTPS request to a public API. The error made no sense — the host was reachable, the cert was valid, the port was 443. But the connection kept getting refused.

I added logging. The resolved IP was an IPv6 address. The target API doesn't support IPv6.

What happened

Apify's container runtime uses dual-stack networking. When a hostname has both A and AAAA records, the sandbox resolves both. Node's fetch (and undici, and axios) will try IPv6 first if it's available. The API server only listens on IPv4. Connection refused. Retries don't help — they just hit IPv6 again.

Locally, my ISP doesn't give me IPv6, so the lookup returns only A records. In CI, GitHub Actions runners are IPv4-only. The sandbox is the only place where dual-stack exists. That's why I couldn't reproduce it.

The fix

Force IPv4 in the HTTP client. For undici (Node 18+ native fetch):

const agent = new undici.Agent({
  connect: { family: 4 } // force IPv4
});
const res = await fetch(url, { dispatcher: agent });

For axios:

const res = await axios.get(url, {
  family: 4, // or: resolver: (host) => dns.resolve4(host)
});

For plain fetch on Node 18+, there's no built-in flag yet. The workaround is a custom Agent or dns.setDefaultResultOrder('ipv4first') at process start (affects everything).

The pattern: environment assumptions leak

This isn't an Apify bug. It's a class of bug where local + CI environments share an assumption (IPv4-only) that the production runtime violates (dual-stack). Other examples:

The habit that catches this

Run a real cloud test run for every deploy, not just "it builds." The two-minute verification against the actual runtime environment catches IPv6, DNS, memory limits, and the occasional API that behaves differently from a datacenter IP. It's the only way to know the code actually works where it lives.

I added family: 4 to my shared HTTP client wrapper. Next actor inherits the fix for free.