Skip to content

TIL: Happy Eyeballs Algorithm

Happy Eyeballs is a simple algorithm that determines whether to use IPv6 or IPv4 for applications that support both protocols.

Let’s hear curl developer Daniel Stenberg explain it in the Changelog podcast:

That’s why we do happy eyeballs, meaning that we do both [IPv6 and IPv4], pretty much. […]

It’s an RFC that actually exists in several versions, but it pretty much means that we actually start the IPv6 attempt first and then a few milliseconds later you start the IPv4 attempt and the one that succeeds first, that’s the one you go with, and then you cancel the other one.

In essence, this algorithm lets you try IPv6 first and fall back to IPv4 without the high latency cost of waiting for the IPv6 attempt to fail first. There’s a YouTube video that explains it in slightly more detail.

IPv4
IPv6
Happy Eyeballs [RFC 8305]
t₀
t₀ + 250ms
time

Figure adapted from Bajpai & Schönwälder, 2016

RFC 8305 is the current version that defines the Happy Eyeballs Version 2. It recommends a default delay of 250 milliseconds between the two connection attempts. There’s an ongoing draft for version 3 too.

I was running an application at localhost:3000 in a tab and then I ran a command to start another application at localhost:3000 from the terminal. I was surprised to see that the second application started successfully. I expected it to either fail or start on a different port like 3001.

When I tried to access the application in the browser from the first tab, I was surprised to see that the second application’s page was displayed. It “hijacked” the port, I thought.

Later I learned about the Happy Eyeballs Algorithm after talking to ChatGPT. I didn’t realize localhost can be either the IPv4 or IPv6 loopback address.

Checking the /etc/hosts file, I found that localhost is mapped to both the IPv4 loopback address 127.0.0.1 and the IPv6 loopback address ::1.

Terminal
$ cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost

So what happened was my first application was running at 127.0.0.1:3000 and the second application was running at [::1]:3000. The algorithm starts the IPv6 attempt first, then the IPv4 attempt. The IPv4 attempt won, so it appeared the second application “hijacked” the localhost port. The key is that localhost resolves to both 127.0.0.1 and ::1, not just 127.0.0.1. Visiting http://127.0.0.1:3000 and http://[::1]:3000 confirmed this.

By the way, [::1]:3000 is another TIL. I didn’t know ::1 is the IPv6 loopback address and also didn’t know you need to use [] in the browser to visit an IPv6 address.