Skip to content
Mail Cabinet Choose a plan

IPv6 for a Website: What It Gives, What Breaks and a Checklist

IPv6 for a Website: What It Gives, What Breaks and a Checklist

A website works over IPv6 when four things are in place: an AAAA record in DNS, a web server listening on an IPv6 address, a firewall that lets ports 80 and 443 through for IPv6, and a certificate that is also served over IPv6. Half of Google’s users already arrive over IPv6 (50.10% on 28 March 2026), so such a site gets a direct path to them. But an AAAA record without the other steps does more harm than no record at all: on our test bench IPv6 clients could not open the site, the browser lost 0.3 s on every connection, a PHP script gave up after 60 s and Let’s Encrypt refused to issue a certificate.

How we tested

10 November 2026. IPv6 is disabled in the kernel of our bench container, so we built a separate User-Mode Linux 6.17 kernel with IPv6 and ran two networks inside it. Server: nginx 1.24, PHP-FPM 8.3, Apache 2.4.58, dnsmasq as DNS. Client: curl 8.5, wget 1.21, Python 3.12, PHP 8.3, headless Chromium 153 via Playwright, plus Pebble 2.4 (the test certificate authority from the Let’s Encrypt team) with Certbot 2.9. Addresses come from the documentation ranges 192.0.2.0/24 and 2001:db8::/32. We broke one thing at a time: listen, firewall, certificate, IP checks in PHP and .htaccess, log parsing. IPv6 shares come from Google (via Internet Society Pulse), APNIC Labs and Common Crawl, checked on 10.11.2026. This is test bench behaviour, not measurements on RichHost servers.

How many visitors already use IPv6

Metric Value Source
Google users reaching Google over IPv6 (world) 50.10% on 28.03.2026, above half for the first time Internet Society Pulse, based on Google IPv6 Statistics
Ukraine, share of IPv6-capable clients 15.3% APNIC Labs, 30-day average, record of 31.10.2026
Poland 19.7%
Germany 72.8%
France 86.0%
Top 100,000 web hosts 38.1% have AAAA; 1.2% have AAAA but did not answer over IPv6 Common Crawl, 16.03.2026

Google counts users of its own services, while APNIC measures a sample through ad impressions, so the numbers differ: Pulse gives 73% for France, APNIC 86%. Google’s page shows only a chart; the APNIC values in the table are 30-day averages from the record of 31.10.2026, checked on 10.11.2026. The conclusion is the same either way: IPv6 is the majority in Western Europe and still a minority in parts of Eastern Europe. One more number from Common Crawl: about one in thirty sites with an AAAA record did not answer over IPv6. That is exactly the case we reproduced.

What IPv6 actually gives a website

  • A direct path to IPv6 users. Mobile carriers with IPv6-only networks reach IPv4 sites through NAT64 translators. A site with a working AAAA record does not depend on them.
  • The real visitor address. Behind carrier-grade NAT (CGNAT) hundreds of subscribers share one IPv4 address, so blocking a password-guessing attempt hits all of them. With IPv6 each subscriber has its own /64 network.
  • What not to expect: a noticeable speedup. On the bench connections over IPv4 and IPv6 took the same 3-15 ms. A gain is possible only where the provider routes IPv4 through an overloaded NAT.

What breaks: seven scenarios from the bench

1. The AAAA record exists, but nginx listens only on IPv4

A configuration with listen 80; and listen 443 ssl; opens IPv4 sockets only. A client that knows both addresses tries IPv6 first, gets refused and moves to IPv4 within milliseconds:

$ curl -v https://site.lab/
*   Trying [2001:db8:20::10]:443...
* connect to 2001:db8:20::10 port 443 from 2001:db8:20::100 port 37444 failed: Connection refused
*   Trying 192.0.2.10:443...
* Connected to site.lab (192.0.2.10) port 443

That is why a site owner on an IPv4 home connection notices nothing. Chromium behaved the same way, but it tried IPv6 again for every new connection. A client with IPv6 only did not open the site at all: curl reported Couldn't connect to server, Chromium ERR_CONNECTION_REFUSED. The fix in nginx:

server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;
}

The single-socket variant listen [::]:80 ipv6only=off; works too, with two catches. A plain nginx -s reload does not change sockets that are already open: on the bench IPv4 simply stopped answering after it, and only a full restart helped. And PHP sees IPv4 visitors as ::ffff:192.0.2.100 (more on that below).

2. The firewall was opened for IPv4, and IPv6 was forgotten

iptables and ip6tables rules are separate. On the bench IPv4 had a DROP policy that allowed ports 22, 80 and 443, while ip6tables had the same policy but allowed only SSH, DNS and ICMPv6. Packets to port 443 over IPv6 were silently dropped, and each client reacted in its own way:

Client What it does when IPv6 is silent Result
curl 8.5, PHP curl extension starts IPv4 in parallel after 200 ms (Happy Eyeballs) 0.20-0.22 s per connection instead of 0.003-0.015 s
Chromium 153 the same with a 300 ms delay 0.30-0.31 s for every new connection
wget 1.21 waits for the system IPv6 timeout, then IPv4 134 s
Python 3.12 urllib the same 133.5 s
PHP file_get_contents() spends the whole default_socket_timeout on IPv6 error after 60.1 s, never reached IPv4

A person in a browser loses 0.3 s, while cron jobs, payment webhooks and plugins that call your site with file_get_contents() hang for minutes or fail. Compare the rules for both protocols and open the same ports:

iptables -S
ip6tables -S
ip6tables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT

If IPv6 is closed on purpose, reject connections instead of dropping them. A plain -j REJECT (ICMPv6 port unreachable) is treated by the Linux kernel as a soft error, and curl gave up only after 1.02 s. With -j REJECT --reject-with tcp-reset the refusal arrives in 1 ms and the browser moves to IPv4 at once.

3. The opposite mistake: IPv6 wide open

It also happens the other way round: someone flushed ip6tables and the policy became ACCEPT. On the bench an internal service on port 8080 was closed for IPv4 (curl waited 5 s and gave up) but answered with 200 over IPv6. Any service listening on [::] becomes reachable from the internet once the server has a public IPv6 address. So check ip6tables -S or nft list ruleset not only after enabling IPv6 but after every rule change.

4. IPv6 serves another site or another certificate

Control panels often bind a site to a specific IPv4 address, while the IPv6 socket on port 443 goes to the default server. On the bench everything was fine over IPv4, but over IPv6 curl reported SSL: no alternative certificate subject name matches target host name. You only see it when you test over IPv6:

$ openssl s_client -connect 192.0.2.10:443 -servername site.lab
subject=CN = site.lab
$ openssl s_client -connect [2001:db8:20::10]:443 -servername site.lab
subject=CN = server.default.lab

IPv6 visitors get a “connection is not private” warning, everyone else sees nothing.

5. Let’s Encrypt checks IPv6 first

During HTTP-01 validation the certificate authority downloads a file from /.well-known/acme-challenge/ itself, and if the domain has AAAA, it goes over IPv6. The test Pebble server on the bench used IPv6 only and never switched to IPv4:

AAAA, nginx only on IPv4:
  Detail: ... dial tcp [2001:db8:20::10]:80: connect: connection refused
ip6tables DROP tcp/80:
  Detail: ... context deadline exceeded (Client.Timeout exceeded while awaiting headers)
[::]:80 served by another site:
  Detail: Non-200 status code from HTTP: http://le3.example.com:80/.well-known/acme-challenge/... returned 404
dual stack:
  Successfully received certificate.

The real Let’s Encrypt is more forgiving. According to the source of its Boulder server (va/http.go, checked on 10.11.2026), it takes the first IPv6 address and falls back to IPv4 only when the connection itself fails: refused or a connect timeout, as long as the overall validation time has not run out. If a server answers over IPv6, even another site with a 404, the result is final. A previous bench run showed the same picture: with an AAAA record, validation started over IPv6. All the ways to get a certificate are covered in our guide to free Let’s Encrypt SSL.

6. PHP, WordPress and .htaccess see an IPv6 address

This is what the same PHP script got from an IPv4 and an IPv6 visitor:

IPv4: REMOTE_ADDR=192.0.2.100        ip2long: 3221226084   v4-only allowlist: ALLOW
IPv6: REMOTE_ADDR=2001:db8:20::100   ip2long: false        v4-only allowlist: DENY
IPv4 via listen [::]:80 ipv6only=off: REMOTE_ADDR=::ffff:192.0.2.100   v4-only allowlist: DENY

Functions like ip2long(), regular expressions for four dot-separated numbers and the FILTER_FLAG_IPV4 filter show up in admin protection plugins, rate limits and geoblocking. With IPv6 they either lock the owner out or let everyone in. A reliable check uses inet_pton() and a prefix comparison, so the list can hold both 192.0.2.0/24 and 2001:db8:20::/64. In WordPress 6.4.3 itself the comment_author_IP column holds 100 characters, but in multisite the wp_registration_log table stores the IP in a 30-character column, while a full IPv6 address takes up to 39.

Apache is no different. The rule Require ip 192.0.2.0/24 in an .htaccess file let IPv4 through with 200 and stopped the IPv6 client with 403 and the line AH01630: client denied by server configuration. Adding our IPv6 network to the same line fixed it:

Require ip 192.0.2.0/24 2001:db8:20::/64

Add the /64 network (or the /56 your provider assigns), not a single address: devices rotate temporary IPv6 addresses, and yesterday’s rule will not match tomorrow.

7. Logs: addresses with colons

The bench log had 16 requests: 8 from an IPv4 client and 8 from one IPv6 visitor rotating temporary addresses within one /64 network. The usual commands gave this:

$ grep -oE '^[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log | sort | uniq -c
      7 192.0.2.100
$ awk '{print $1}' access.log | awk -F: '{print $1":"$2":"$3":"$4}' | sort | uniq -c
      7 192.0.2.100:::
      2 2001:db8:20:
      6 2001:db8:20:0
      1 ::ffff:192.0.2.100

The first pattern lost 9 of 16 lines, the second split one network in two because 2001:db8:20::100 is written in compressed form. In the Apache error log the address comes with the port ([client 2001:db8:20::100:34996]), and awk -F: returns just 2001. Cut the port at the last colon and group addresses with Python’s ipaddress module:

awk '{print $1}' access.log | python3 -c '
import sys, ipaddress, collections
c = collections.Counter()
for line in sys.stdin:
    ip = ipaddress.ip_address(line.strip())
    if ip.version == 6 and ip.ipv4_mapped: ip = ip.ipv4_mapped
    c[str(ipaddress.ip_network(f"{ip}/{64 if ip.version == 6 else 32}", strict=False))] += 1
for net, n in c.most_common(): print(n, net)'

Result: 8 192.0.2.100/32 and 8 2001:db8:20::/64, two visitors, exactly as it was. How to read access and error logs in general is covered in our guide to server logs on hosting.

Checklist: what to check, command, expected result

Run the checks from a machine that has IPv6. Without an IPv6 route, Chromium on the bench did not even ask for AAAA and went over IPv4, so from a laptop without IPv6 the problem is invisible.

What to check Command Expected result
AAAA record dig +short AAAA example.com your server’s IPv6 address; empty if you have not enabled IPv6
Server listens on IPv6 ss -ltn lines [::]:80 and [::]:443 or your IPv6 address
nginx configuration nginx -T | grep listen listen [::]:443 ssl in the block of your own domain
Firewall ip6tables -S or nft list ruleset tcp 80 and 443 allowed for IPv6 as for IPv4; no extra open ports
Site answers over IPv6 curl -6 -sI https://example.com/ the same status (200 or 301) and time as curl -4
Certificate over IPv6 openssl s_client -connect [IPv6]:443 -servername example.com your domain in subject, Verify return code: 0 (ok)
Let’s Encrypt validation path curl -6 http://example.com/.well-known/acme-challenge/test the test file from your site, same as with curl -4
IP in PHP and admin protection a script with echo $_SERVER['REMOTE_ADDR']; opened over IPv6 an IPv6 address without ::ffff:; the admin area lets you in over IPv6
Logs and monitoring awk '{print $1}' access.log | grep -c : IPv6 requests are there and not lost in reports; monitoring has a separate IPv6 check

If X, do Y

Situation What to do
Your hosting gives no IPv6 address Do not add AAAA. IPv6 subscribers will reach the site through their carrier’s NAT64
AAAA exists and curl -6 instantly says “Couldn’t connect” Add listen [::] or remove AAAA until the server is ready
curl -6 hangs for 10 s or more The firewall silently drops IPv6: open 80 and 443 in ip6tables or nftables
Let’s Encrypt mentions [IPv6]:80 Fix IPv6, or remove AAAA for now and wait for the TTL to expire
Only some visitors get a certificate warning Test openssl s_client over IPv6, bind the certificate to the IPv6 socket
Admin area or API restricted by IP list Add your IPv6 /64 or /56 network and check that the plugin understands IPv6
Rate limits, bans and reports count single addresses Group IPv6 by /64 network

An AAAA record is added and removed in the same DNS zone as the other DNS records, and the change is not instant: the old record stays in caches until its TTL expires. How long that takes in practice we measured in our article on DNS propagation time.

IPv6 on hosting: what to ask support

On shared hosting the provider manages addresses, listen and the firewall, and you are left with DNS and the site code. Ask support four things: is there an IPv6 address for the site on your plan, is it bound to the domain in the control panel, does the panel add the AAAA record itself, and does Let’s Encrypt issuance work over IPv6. RichHost hosting plans include our own control panel with an AI assistant and free Let’s Encrypt SSL; whether an IPv6 address is available for your site, ask support (open 10:00 to 20:00 Kyiv time, replies in about 30 minutes). Then run the checklist above.

FAQ

Do I need IPv6 if my audience is in a country with low adoption?

Not necessarily: APNIC measures about 15% IPv6-capable clients in Ukraine and 20% in Poland, and IPv4 serves the rest. But if the AAAA record already exists, it must work, or those visitors suffer first. For audiences in Germany or France IPv6 is already the main protocol.

Will IPv6 make my site faster?

Not by itself: on the bench connections over both protocols took 3-15 ms. A visible difference appears only when IPv6 is misconfigured, and then it favours IPv4.

Why does the browser open the site while a cron job or script hangs?

Browsers and curl try IPv4 in parallel after 200-300 ms. Many other clients (wget, Python urllib, PHP file_get_contents()) wait for the IPv6 timeout: 60-134 seconds on the bench.

Is it safe to just delete the AAAA record?

Yes, if the server is not ready for IPv6, it is the simplest way back: clients will use IPv4 once the old record’s TTL expires. If the browser shows DNS_PROBE_FINISHED_NXDOMAIN, the problem is not IPv6 but the domain itself: see our DNS_PROBE_FINISHED_NXDOMAIN fix.

More on this topic

All articles
SSH Keys: How to Create Them, Pick a Type and Disable Passwords
Hosting Guides and Tutorials27.07.2026

SSH Keys: How to Create Them, Pick a Type and Disable Passwords

How to create an SSH key, why Ed25519 beats RSA for new keys, how to add it to a server, a hosting…

Fail2Ban and CSF Setup: SSH, nginx and WordPress Jails, Tested
Hosting Guides and Tutorials01.07.2026

Fail2Ban and CSF Setup: SSH, nginx and WordPress Jails, Tested

How to configure Fail2Ban for SSH, nginx and WordPress, what it really blocks, and six pitfalls: locking yourself out, IPv6, journald, Cloudflare…

Website Security Audit Checklist: One Hour, Real Commands
Hosting Guides and Tutorials11.06.2026

Website Security Audit Checklist: One Hour, Real Commands

How to audit a website on shared hosting or your own VPS in one hour: exposed .git, .env and database dumps, WordPress…