Almost every rate limiter, login throttle, and IP blocklist in production was designed against IPv4, where the mental model is simple: one address, roughly one user. Count requests per address, ban the address, move on. That model has quietly become wrong. The moment your traffic includes IPv6 — and a large and growing share of it does — the unit you are counting on stops mapping to a person, and every defense built on "one address" springs a leak.
This is not a subtle edge case. It is the difference between a rate limiter that works and one that an attacker walks straight through while your dashboards show a tidy row of single-request addresses. The fix is not more rules. It is counting the right thing.
The IPv4 Habit That Breaks on IPv6
On IPv4, address scarcity did you a favor. There are only about 4.3 billion addresses, they are expensive, and the overwhelmingly common arrangement is that a subscriber gets a single public address at a time. So "requests from this /32" and "requests from this user" were close enough to treat as the same thing. Ban the /32 and you inconvenience, at most, the household or the NAT behind it.
IPv6 removes the scarcity, and with it the assumption. The address is 128 bits instead of 32 — eight groups of four hex digits — and the design philosophy is the opposite of frugal. Instead of rationing single addresses, IPv6 hands every customer a block of them. The smallest block anyone gets is enormous, and that block, not the individual address, is the real identity of the user.
If your limiter still counts per full address on IPv6, an abuser does not need a botnet or a proxy pool. They need one ordinary IPv6 allocation and a for-loop.
One Subscriber, 18 Quintillion Addresses
Here is the number that reframes the whole problem. The smallest routable block an IPv6 end user is normally assigned is a /64 — the first 64 bits are the network, the last 64 bits are theirs to fill however they like. That leaves 2^64 usable addresses inside a single subscriber's subnet:
2^64 = 18,446,744,073,709,551,616 addresses — roughly 18.4 quintillion, all belonging to one user, all equally routable.
Two facts make this unavoidable rather than a quirk. First, stateless address autoconfiguration (SLAAC), the standard way IPv6 hosts self-assign addresses, requires a full /64 per subnet — you cannot hand out anything smaller to a normal LAN. Second, the address-planning guidance the RIRs follow (RFC 6177 / BCP 157) tells ISPs to give each end site a generous allocation — commonly a /56 or /48, and never less than a /64. Abundance is the whole point of the protocol.
Now the attacker's side. Cycling through those addresses is not expensive or clever — it is a built-in socket option. The Linux IP_FREEBIND flag lets a process bind to and send from any address inside a locally routed prefix without configuring each address on an interface first, and public tooling such as the widely-referenced freebind project wraps exactly that into "use a fresh source address for every connection." Give it a routed /64 and it will emit traffic from 18 quintillion distinct sources at no cost.
So the per-/128 rate limit does not slow this down by a factor of anything. Every request looks like a brand-new address seen once. Your limiter's counters never increment past one, your blocklist fills with millions of single-hit entries and matches nothing, and the attack proceeds at full speed while looking, on paper, perfectly distributed.
The IPv6 Allocation Hierarchy, and What Each Level Means
To limit the right unit you have to know what each prefix length actually represents. IPv6 prefixes are just CIDR blocks — the same /n notation as IPv4, where a smaller number covers more addresses — but the conventional boundaries carry meaning about who is on the other side.
The shape of the decision falls out of the table. The /128 is never the unit. The /64 is the floor — the tightest scope that still corresponds to one actor, and therefore the default. Everything wider is an escalation you reach for only when you have evidence that the broader allocation, as a whole, is the problem.
Rule One: Key Everything on the /64
The single change that fixes most of the breakage is to stop counting per address and start counting per /64. Concretely, for every incoming IPv6 request, derive the /64 by keeping the first four hextets (64 bits) and use that as the key for rate-limit counters, login-attempt tallies, and blocklist lookups.
// Normalize any IPv6 address to its /64 aggregation key.
// The first 64 bits (4 hextets) identify the subscriber's subnet;
// the last 64 bits are theirs to rotate through, so we discard them.
function ipv6Key(addr) {
// Expand "2001:db8::1" to full form, take the first four groups.
const full = expandIPv6(addr); // 8 groups of 4 hex digits
return full.split(":").slice(0, 4).join(":") + "::/64";
}
// "2001:0db8:85a3:0000:0000:8a2e:0370:7334" -> "2001:0db8:85a3:0000::/64"
Every mainstream edge tool already supports this — you rarely have to build it yourself:
- nginx — build the limit key from a
map/geoblock that masks IPv6 to /64, then feed it tolimit_req_zone/limit_conn_zoneinstead of$binary_remote_addr. - HAProxy — set a 64-bit mask on the source when you build stick-table keys, so
srcis aggregated to the /64 rather than the full address. - Cloudflare and most CDNs / WAFs — already treat IPv6 in /64 blocks for their own rate-limiting and challenge logic; mirror that granularity in your origin rules so the two layers agree.
- Your own app limiter — swap the raw
remote_addrfor the /64 key above before it ever reaches Redis, the token bucket, or the counter.
Do only this, and the free-rotation attack collapses back to what it should be: 18 quintillion addresses become one counter, and the abuser is throttled exactly as if they were a single IPv4 address. This is the highest-leverage line of code in the whole topic.
Rule Two: Escalate to /56 and /48 on Persistence
Keying on the /64 handles the attacker who rotates addresses inside one subnet. The next move up the ladder is the attacker who rotates subnets — because a subscriber often holds not one /64 but a whole /56 or /48 full of them, and freebind-style tooling can walk across those too.
The signal to widen is persistence: fresh /64s that keep misbehaving while sharing a common parent prefix. When you see that, the whole allocation is one entity and should be scoped as one.
Think of it as a counter that rolls up. Maintain the /64 tally as the base unit, but also track abuse aggregated to each /56 and /48. When a parent prefix crosses a threshold that no single legitimate customer would — many distinct child /64s all offending in a short window — apply the limit at the parent level and let it cover every child at once. That is how you stay ahead of an attacker who owns a large allocation without punishing the ones who own a small, well-behaved one.
Rule Three: Widen by Network Type, Not by Reflex
Escalation has a failure mode, and it is the expensive one: widen too far, and a single ban erases thousands of innocent users. A /48 might be one business — or it might be a slice of a mobile carrier. A /32 is almost never one actor; it is typically an entire ISP or hosting provider. The persistence signal tells you that the parent prefix is involved; it does not tell you how many real people sit behind it. For that, you need to know what kind of network the prefix is.
This is exactly where an IP lookup earns its place in the pipeline. One call on a representative address from the offending prefix tells you whether you are looking at rented infrastructure you can aggregate aggressively, or consumer access you must treat gently.
curl -H "x-api-key: YOUR_API_KEY" \
https://api.geoiphub.com/v1/lookup/2001:db8:85a3::8a2e:370:7334
The fields that govern how wide you may safely go:
const res = await fetch(`https://api.geoiphub.com/v1/lookup/${addr}`, {
headers: { "x-api-key": process.env.GEOIPHUB_API_KEY },
});
const ip = await res.json();
ip.asn.asn_type; // "hosting" | "isp" | "mobile" | "cdn" ...
ip.asn.connection_type; // "datacenter" | "residential" | "mobile"
ip.asn.carrier; // set for cellular networks — a leniency signal
ip.detection.is_hosting; // true — infrastructure, aggregate up freely
ip.threat.is_cgnat; // shared address space — do not widen a hard ban
ip.scoring.fraud_score; // 0–100 composite, when you must decide per-address
ip.scoring.recommended_action; // "allow" | "review" | "block"
ip.scoring.detection_methods; // exactly which signals fired — your audit trail
The reading is directional, and it mirrors the same principle that governs IP risk scoring across 40+ signals: some fields let you act harder, others tell you to hold back.
The discipline is the one we make the full case for in detecting anonymized traffic without blocking real customers: on a hosting or datacenter prefix you can widen the block with confidence, because a checkout or a signup has no honest reason to originate from a rack of servers — the same tell we unpack in what a datacenter IP address is. On a mobile or residential prefix you keep the scope tight and let the risk score, not a wide CIDR ban, carry the load. Widening by reflex is how a rate limiter quietly becomes an outage for real users.
Rule Four: Let Prefix Reputation Decay
The last rule is about time. IPv4 blocklists already go stale because addresses get reassigned; IPv6 makes it worse, because whole prefixes are delegated, returned, and re-delegated, and the /64 that hammered your login page last month may belong to an ordinary household today. A permanent blocklist on rotating address space slowly becomes a list of places you can no longer safely reach.
So do not store a permanent verdict per prefix. Store a reputation that moves:
- Raise on abuse. Each offending request bumps the /64's score, and rolls a smaller increment up to its /56 and /48 parents.
- Decay over time. If a prefix stays quiet, its score falls on a schedule — hours for a soft throttle, days for a hard block — until it crosses back under your threshold and the block lifts itself.
- Act on the current score, not on history. Rate-limit, challenge, or block based on where the reputation sits now, so a reformed or reassigned prefix recovers automatically instead of staying on a list nobody prunes.
- Keep the evidence. Store which signals drove each score change — the
detection_methodsfrom the lookup, the persistence pattern — so a wide block is a decision you can explain, not a mystery entry.
Decay is what keeps the whole system honest. It means an aggressive escalation during an attack does not become a permanent scar, and it means your blocklist reflects who is hostile right now rather than who was hostile once.
Getting Started This Week
- Switch the limiter key to the /64. Everywhere you currently count or block per address, mask IPv6 to its first 64 bits first. This one change closes the free-rotation hole and is usually a few lines in nginx, HAProxy, or your app.
- Add rolled-up counters for /56 and /48. Track abuse aggregated to the parent prefixes so a subnet-rotating attacker trips a wider limit — without you widening by default.
- Look up the network before you widen. On any escalation past /64, check
asn_type/connection_type: aggregate freely on hosting, stay narrow on mobile and residential, never hard-block a carrier or CGNAT range. - Score the ambiguous cases. Where a prefix is shared or consumer, use the composite
fraud_scoreandrecommended_actionper address instead of a blunt CIDR ban. - Give every block a decay. Store prefix reputation with a half-life, act on the current score, and let reassigned space clear itself automatically.
IPv6 did not make abuse prevention harder so much as it changed the unit. The address is no longer the user; the /64 is. Count that, escalate by evidence, weight by network type, and let time do the forgiving — and every defense you already trust on IPv4 works again.
Start Scoring Every IP in Real Time
GeoIPHub gives fraud, security, and engineering teams a single API for IP geolocation, VPN & proxy detection, threat intelligence, and an explainable 0–100 risk score.
Get Your Free API Key
Sign up in minutes — no credit card required. Upgrade only when you need more volume.
Frequently Asked Questions
Why can't you rate-limit an IPv6 address the way you do an IPv4 address?
Because the two protocols hand out a very different amount of address space per user. On IPv4, one subscriber usually gets one address, so limiting per /32 (a single address) limits per user. On IPv6, a single subscriber is routinely allocated an entire /64 — that is 2^64, roughly 18.4 quintillion addresses — and tools like IP_FREEBIND let a process source traffic from any address in that block for free. Rate-limiting or blocklisting one /128 (single IPv6 address) therefore stops nothing: the abuser just uses the next of 18 quintillion addresses. The correct unit on IPv6 is the /64, not the /128.
What prefix length should I rate-limit IPv6 on?
Start at the /64. The /64 is the smallest block guaranteed to belong to a single end user, because SLAAC requires a full /64 per subnet and ISPs allocate at least a /64 to every customer. Key your counters and blocklists on the first 64 bits of the address and treat the whole /64 as one actor. When abuse persists across many /64s from the same customer, escalate the scope to the /56 (256 /64s) and then the /48 (65,536 /64s), which are the typical sizes of a whole home or business allocation.
Is it safe to block an entire IPv6 /64?
Almost always yes — safer than blocking a single IPv4 address. A /64 is designed to be one subscriber's subnet, so blocking it rarely catches an unrelated bystander the way a shared IPv4 CGNAT address does. The real risk is at the wider prefixes: a /48 or /32 can be a whole ISP, a mobile carrier, or a shared-hosting range covering thousands of unrelated users. Widen the scope only when the network type justifies it — aggressively on hosting/datacenter prefixes, cautiously on mobile and residential ones.
How do I know how wide to escalate a block?
Let two things decide it: persistence and network type. Persistence — if abuse keeps reappearing from fresh /64s inside the same /56 or /48, that entire allocation is one entity and you widen to it. Network type — a lookup that returns a hosting or datacenter ASN means the prefix is infrastructure and you can aggregate up quickly; a mobile carrier or residential ISP means many real users may sit behind the wider prefix, so you stay narrow and lean on a risk score instead of a hard ban.
Do CGNAT and mobile networks change how I should handle IPv6?
Yes, at the wide end. A single IPv6 /64 is normally one subscriber even on mobile, so per-/64 limiting is still correct. But mobile carriers and 464XLAT/NAT64 deployments can place many subscribers under a shared wider prefix, so a /48- or /32-wide ban on a mobile ASN can take out a large slice of a carrier's customers. The rule is the same as on IPv4: never hard-block a shared range on the strength of one bad actor — score it, and reserve wide bans for infrastructure prefixes.
How long should an IPv6 prefix stay blocked?
Not forever. IPv6 prefixes get reassigned, and a /64 that abused you last month may be a different customer next month. Attach a decaying reputation to each prefix rather than a permanent entry: raise its score on abuse, let the score fall over time if nothing bad happens, and drop the block automatically once it decays below your threshold. A permanent blocklist on rotating address space slowly turns into a list of addresses you can no longer safely reach.
