“My internet feels unstable” was a half-disabled IPv6 stack, not the ISP

The pings were clean. Zero packet loss to anywhere I 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.

This is the story of a bug I had been carrying for years without knowing it, because the thing I did to “fix” IPv6 disabled half of it and left the other half running. The half that stayed on was exactly the half that breaks page loads.

The healthy-on-paper trap

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

So I ran the usual sweep, and the usual sweep lied to me.

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

Here is the gap that matters, and the reason a clean diagnostic sweep is so misleading: your TCP handshake succeeding 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 is doing something a one-off probe is 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 my machine, IPv6 was the trap. The hostname returned a AAAA record. The browser would prefer that v6 address, open a socket to it, and wait. 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, and each one pays the IPv6-attempt-then-fallback tax. Stack that penalty across an entire SPA’s request waterfall and the page simply never finishes assembling. Meanwhile ping, which is ICMP and which I was sending to IPv4 addresses, looked perfect the whole time.

The smoking gun

Once I stopped trusting the “healthy” report and started asking the right 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 was asking DNS for IPv6 addresses, the OS was preferring them, the browser was opening 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 ever see that combination, you have found the half-disabled trap.

Why it was “half” disabled

Here is the part that had been hiding for years. I had, at some point in the past, 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 I 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 I had created the worst possible state. The transport was unbound on the adapters I 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.

That is the lesson that took years to surface: the adapter checkbox disables a transport, not the behavior. If you want Windows to stop preferring IPv6, you have to tell the stack, not the adapter.

The fix is one registry value

The actual 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 does not rip IPv6 out of the machine. 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 not optional, the preference logic loads at boot.

I 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, not capability. If you do not have a v6-dependent service like Tailscale, 0xFF is fine. If you do, reach for 0xFF and you will fix one thing and break 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 takeaways

Two things worth carrying out of this.

First, unchecking “IPv6” in adapter properties is not turning IPv6 off, it is a trap that disables the transport while leaving the stack fully running. AAAA lookups, Happy Eyeballs preference, and v6 socket attempts all keep happening. If you want IPv4 preferred on Windows, set DisabledComponents, do not touch the checkbox. And mind the v6-dependent services on the box: 0x20 to prefer v4 and keep them, 0xFF only when nothing needs v6.

Second, and this is the one that generalizes past Windows: a clean ping, fast DNS, and a successful TCP handshake do not prove a browser can load the page. Those probes test IPv4 paths and single connections. A browser races address families and fans out a request waterfall, and it can stall on a black-holed v6 socket while every diagnostic you ran reports green. When the network looks healthy and the page still will not load, stop trusting the green and ask what the browser is doing that your probe is not.

The bug was a setting I had tried to turn off years ago and never fully did. The fix took one DWord. Finding it took learning to distrust a healthy report.