Rate Limiting by ASN: Smarter Than Per-IP

Per-IP rate limits are noisy for CGNAT and shared infrastructure. Per-ASN rate limiting groups by network operator — and matches reality better.

Rate Limiting by ASN: Smarter Than Per-IP

Per-IP rate limiting is the default for most APIs and applications. It’s simple and works for the obvious cases. But it has well-known weaknesses: CGNAT puts thousands of users on one IP; corporate proxies put hundreds of employees on a few IPs; cloud workloads occupy entire IP ranges. The single-IP unit doesn’t always match reality.

Per-ASN rate limiting is a refinement: rate-limit by network operator instead of by individual IP. Often more accurate, sometimes essential.

This post covers when ASN-based limiting helps, when it doesn’t, and how to combine it with per-IP and per-account limits.

Why Per-IP Falls Short

A few problem cases:

Mobile users on CGNAT

T-Mobile, Verizon Wireless, Vodafone — large mobile carriers share single public IPs among thousands of subscribers. A per-IP limit of “100 requests per minute” affects 1000 users sharing one IP. Even legitimate use looks abusive.

Corporate networks

A 5000-employee company exits through a small NAT pool. From your service’s perspective, 5000 users look like 5 IPs. Per-IP limits are far too restrictive.

Cloud / bot networks

A scraper running on AWS uses many EC2 instances with different IPs. Per-IP limits don’t slow it down because each instance gets its own quota.

Distributed attacks

A botnet uses thousands of compromised home routers. Each has a unique residential IP. Per-IP limits don’t notice.

In all these cases, per-IP is either too strict (CGNAT, corporate) or too lenient (cloud, botnet).

What Per-ASN Does Differently

ASN is the network operator: T-Mobile is one ASN; Comcast is another; AWS is another.

By rate-limiting per-ASN instead of per-IP:

  • CGNAT mobile traffic has a single high quota that all subscribers share. Individual users still effectively get a share; the quota matches the carrier’s scale.
  • Corporate traffic shares a quota. A company with 5000 employees gets a reasonable shared quota.
  • AWS traffic shares a quota. A scraper using 100 EC2 instances can’t get 100x the quota of a single client.

The unit matches the underlying network reality better than per-IP does.

Implementation Pattern

A simple version:

async function isAllowed(ip: string, limit: number, windowMs: number): Promise<boolean> {
    const result = await convertIP(ip)
    const key = result.success ? `asn:${result.data.asn.number}` : `ip:${ip}`
    
    const count = await redis.incr(key)
    if (count === 1) await redis.pexpire(key, windowMs)
    
    return count <= limit
}

Resolve IP to ASN via Ip2Geo (or your geolocation provider); use the ASN as the rate-limit key. Cache ASN lookups; they don’t change.

Tiered approach

Different ASN types deserve different limits:

const ASN_TYPE_LIMITS = {
    'residential': 100,
    'mobile':      1000,  // CGNAT carriers need big shared quotas
    'hosting':     50,    // bots and automation; tighten
    'tor':         10,    // strong restriction
    'unknown':     50,
}

async function getLimit(ip: string): Promise<number> {
    const result = await convertIP(ip)
    if (!result.success) return ASN_TYPE_LIMITS.unknown
    return ASN_TYPE_LIMITS[result.data.asn.type] ?? ASN_TYPE_LIMITS.unknown
}

Now hosting traffic gets tighter limits than residential; mobile gets a larger shared quota.

Combining Per-ASN and Per-IP

Best practice: both. Per-ASN catches the patterns per-IP misses; per-IP catches the patterns per-ASN misses.

async function checkRateLimits(ip: string): Promise<boolean> {
    const ipKey = `rl:ip:${ip}`
    const asn = await getAsn(ip)
    const asnKey = `rl:asn:${asn}`
    
    const [ipCount, asnCount] = await Promise.all([
        redis.incr(ipKey),
        redis.incr(asnKey)
    ])
    
    if (ipCount === 1) redis.pexpire(ipKey, WINDOW_MS)
    if (asnCount === 1) redis.pexpire(asnKey, WINDOW_MS)
    
    if (ipCount > PER_IP_LIMIT) return false
    if (asnCount > PER_ASN_LIMIT) return false
    
    return true
}

Per-IP limit catches noisy single IPs. Per-ASN limit catches distributed abuse from one network operator.

When Per-ASN Doesn’t Work

A few cases where ASN-based limits are inappropriate:

Per-user features

If your service is authenticated and you have user accounts, per-account rate limits are more accurate than network-level ones. Per-ASN is for unauthenticated traffic.

Global services like Cloudflare

If an attacker uses Cloudflare’s Workers or another service whose ASN is also used by legitimate users (e.g., your own customers behind Cloudflare), you can’t aggressively limit by ASN without hurting legitimate users.

Very small ASNs

If an ASN serves only a few customers, per-ASN limit ~ per-IP limit. No gain.

Per-Account vs Per-ASN

For authenticated APIs:

  • Per-account is the right default. Limits scale with the user’s plan.
  • Per-ASN is supplementary for “is this account being abused from a high-risk network?”

For unauthenticated public endpoints:

  • Per-IP + per-ASN is the right pair.

Real-World Limits

Sample limits for a public-facing API:

PER_IP_LIMIT_PER_MIN:        100
PER_ASN_LIMIT_PER_MIN_MOBILE: 100_000   // mobile carrier
PER_ASN_LIMIT_PER_MIN_RES:    50_000    // residential ISP
PER_ASN_LIMIT_PER_MIN_HOSTING: 1_000    // cloud / hosting
PER_ASN_LIMIT_PER_MIN_TOR:    100       // tor exits

A single residential user gets 100/min. Their household (sharing an IP) shares 100/min — fine for normal use. The whole residential ISP gets 50k/min — handles legitimate user count.

A single AWS instance gets 100/min. All AWS traffic gets 1000/min — if a scraper spins up 100 instances, they collectively get 1000/min, not 10,000.

Tune the numbers to your application’s traffic profile.

Caching ASN Lookups

The ASN for an IP changes rarely — typically only when the IP block is transferred between ASNs. Cache aggressively:

const asnCache = new LRUCache<string, number>({ max: 100_000, ttl: 86_400 * 1000 })

async function getAsn(ip: string): Promise<number | null> {
    const cached = asnCache.get(ip)
    if (cached !== undefined) return cached
    
    const result = await convertIP(ip)
    const asn = result.success ? result.data.asn.number : null
    asnCache.set(ip, asn ?? -1)
    return asn
}

Cache hits hugely reduce API calls. For details, see caching IP geolocation responses.

Response Patterns

When rate-limiting hits:

429 Too Many Requests

The standard. Include Retry-After and X-RateLimit-* headers:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1700000000
X-RateLimit-Reason: per_asn_limit

Soft degradation

Instead of rejecting, slow down or return cached/cheaper responses. Better UX for borderline cases.

Challenge

Issue a CAPTCHA or bot challenge. If the requestor is human, they pass; bots don’t.

Observability

For rate-limiting effectiveness:

  • Log rejection events with IP, ASN, ASN type, endpoint.
  • Dashboard showing rejection counts by ASN type.
  • Alert on sudden spikes — large rejections may indicate either an attack or a misconfigured limit.

This data informs tuning: if you’re rejecting a legitimate ISP, raise its limit. If you’re allowing a known-bad ASN through, tighten.

When the Per-ASN Approach Saved Production

A real pattern: an API was getting hammered by scrapers running on AWS. Per-IP limits weren’t catching them because each EC2 instance had a fresh IP. Adding per-ASN limits — specifically aggressive limits on hosting ASNs — stopped the scraping cold without affecting any legitimate users (whose traffic was on residential ASNs).

This pattern repeats: per-ASN is the right unit for hosting traffic. For consumer traffic, per-IP or per-account is usually fine.

Considerations and Caveats

CGNAT-heavy markets

In markets where most consumer traffic is on CGNAT (some mobile, some emerging economies), the line between “per-IP” and “per-ASN” blurs. Both are coarse-grained.

Hosting providers with mixed use

AWS hosts both bots and legitimate applications. Aggressively limiting AWS traffic also hurts legitimate AWS-hosted apps that integrate with your service. Use ASN-type-based tiers, not pure ASN allowlists.

Cloudflare and other meta-ASNs

Some ASNs (Cloudflare’s AS13335) serve as proxy for many sources. Your view of the source IP is Cloudflare’s IP. Both per-IP and per-ASN treat all Cloudflare-proxied traffic alike. Use the CF-Connecting-IP header to get the real source.

TL;DR

  • Per-IP rate limits are too strict for CGNAT/corporate, too lenient for cloud/bots.
  • Per-ASN rate limits group by network operator — more accurate unit.
  • Tier by ASN type: residential, mobile, hosting, VPN, Tor — different limits.
  • Combine per-IP and per-ASN for robust rate limiting.
  • For authenticated APIs, per-account beats both. Per-ASN supplements.
  • Cache ASN lookups aggressively — they rarely change.
  • Tune limits empirically — start permissive, tighten based on rejections.
  • Use 429 with Retry-After for rejections.

Per-ASN rate limiting is one of those refinements that’s easy to add (one geolocation API call per request) and meaningfully improves protection for the common abuse patterns. For the general rate-limiting algorithms, see rate limiting algorithms; for the specific IP-lookup API use case, rate limiting IP lookup API. The Ip2Geo API returns ASN classification inline, making per-ASN rate limiting one extra step beyond per-IP.

Get Started

Convert IPs into accurate location data in milliseconds.

Sign up today and get 1,000 free monthly stored conversions, and discover why developers trust us for fast, reliable, and affordable IP conversions.