The pings were clean. Zero packet loss to anywhere we tested, DNS resolved in milliseconds, and a TCP handshake to port 443 completed fast. By every diagnostic a normal person runs, the network was healthy. And the page still would not load, in either browser, on a machine that could ping the entire internet.

We had been carrying this bug for years without knowing it. The thing we did to “fix” IPv6 disabled half of it and left the other half running, and the half that stayed on is the half that breaks page loads.

The healthy-on-paper trap

It started as a vague complaint: “our internet seems unstable.” Specific sites would stall forever while most of the web was fine. The sites that stalled had something in common that we did not notice at first: they were dual-stack and Cloudflare-fronted. Mixcloud was the reproducible one.

So we ran the usual sweep, and the usual sweep lied to us.

Ping was clean. A 30-probe run showed 0 dropped packets. The only odd thing was jitter: average latency around 31ms, a standard deviation of 33ms and a worst case of 106ms. The variance was high and nothing was lost. A traceroute looked alarming until we read it carefully. Every hop’s first probe came back around 50ms and the next two around 4ms. That pattern is an idle link waking up, classic bufferbloat at the ISP edge. It was real, and it was a red herring. It had nothing to do with why a page would not render.

A clean sweep misleads because a successful TCP handshake does not mean a browser can load the page. A HEAD request to the host came back 200 in 342ms while the actual browser stalled forever, because the browser does something a one-off probe does not.

Happy Eyeballs is the part that bites

Modern browsers do not just connect to a site. When a hostname has both an A record (IPv4) and a AAAA record (IPv6), the browser races both, a behavior called Happy Eyeballs. On a healthy dual-stack machine, whichever connects first wins, and you never notice.

On our machine, IPv6 was the trap. The hostname returned a AAAA record. The browser preferred that v6 address, opened a socket to it, and waited. There was no IPv6 default route on the box, so that socket went nowhere and eventually failed with “network unreachable” before the browser fell back to IPv4.

For a single page that penalty might be survivable. But a modern site fans out into dozens of subresource requests: scripts, fonts, images, API calls. Each one pays the IPv6-attempt-then-fallback tax. Stack that across an entire SPA’s request waterfall and the page never finishes assembling. Meanwhile ping, which is ICMP and which we were sending to IPv4 addresses, looked perfect the whole time.

The smoking gun

Once we stopped trusting the “healthy” report and started asking different questions, the picture assembled in about four commands.

Were AAAA records coming back? Yes:

Terminal window
Resolve-DnsName www.mixcloud.com -Type AAAA

Was there an IPv6 default route? No. This returned nothing:

Terminal window
Get-NetRoute -AddressFamily IPv6 | Where-Object DestinationPrefix -eq '::/0'

Could the machine reach any public IPv6 at all? No. A ping to Cloudflare’s resolver at 2606:4700:4700::1111 failed:

Terminal window
Test-NetConnection -ComputerName 2606:4700:4700::1111 -InformationLevel Quiet

So the machine asked DNS for IPv6 addresses, the OS preferred them, the browser opened sockets to them, and every one of those sockets black-holed. That is the whole bug: AAAA records returned, no v6 default route, ping to public v6 fails. If you see that combination, you have found the half-disabled trap.

Why it was only half disabled

Here is the part that had been hiding. At some point in the past we had gone into the Wi-Fi adapter properties and unchecked “Internet Protocol Version 6,” fully expecting that to turn IPv6 off for browsing. It did not do what we thought.

Unchecking that box stops that adapter from passing IPv6 frames. That is all it does. It does not stop:

  • the DNS resolver from asking for AAAA records
  • Windows from preferring IPv6 destinations in Happy Eyeballs
  • browsers from racing v6 sockets that then fail “network unreachable”

So we had created the worst possible state. The transport was unbound on the adapters we browse through, so v6 traffic had nowhere to go, but the stack was fully on, so the OS kept choosing v6 first and waiting for it to fail. Per-adapter ms_tcpip6 was unchecked on Wi-Fi and Ethernet, but HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\DisabledComponents was never set. The checkbox and the registry value are two different controls, and only the registry value governs the preference logic.

The adapter checkbox disables a transport and leaves the behavior in place. If you want Windows to stop preferring IPv6, you have to tell the stack, not the adapter.

The fix is one registry value

The fix is a single DWord:

Terminal window
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters' -Name DisabledComponents -Value 0x20 -Type DWord
Restart-Computer

0x20 is the documented “prefer IPv4 over IPv6” bit. It leaves the v6 stack functional for anything that explicitly binds to a v6 address, and it stops the OS from reaching for v6 first on general internet destinations. The reboot is required because the preference logic loads at boot.

We deliberately did not use 0xFF, which fully disables IPv6 across the machine including loopback. On this box that would break Tailscale, which depends on IPv6 (fd7a:115c:a1e0::/48). 0x20 preserves Tailscale’s v6 because that traffic is explicitly bound to v6 addresses, and 0x20 changes preference and leaves capability alone. If you do not have a v6-dependent service like Tailscale, 0xFF is fine. If you do, reaching for 0xFF fixes one thing and breaks another.

To verify after the reboot:

Terminal window
Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters' -Name DisabledComponents
# Expect: DisabledComponents : 32

(0x20 reads back as decimal 32.)

The bug was a setting we had tried to turn off years ago and never fully did. The fix took one DWord. Finding it meant checking the three things a browser depends on that ping never touches: AAAA records, an IPv6 default route, and reachability of public IPv6.