Skip to content

IP-Based Rate Limiting — Per-Address Traffic Control for APIs

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about IP. We cover key concepts, practical examples, and best practices to help you master this topic.

IP-based Rate Limiting restricts requests based on the client's IP address, using the real client IP behind proxies, supporting IPv4 and IPv6, and grouping related addresses with CIDR ranges.

What You'll Learn

  • Extracting the real client IP behind proxies
  • IPv4 and IPv6 rate limit handling
  • CIDR-based grouping for shared IPs (NAT, office networks)

Why It Matters

IP-based rate limiting is the most basic and universal approach. Every client has an IP address, making it easy to implement without authentication. However, IPs can be shared (NAT, VPN, corporate networks) or easily changed (mobile, cloud).

Real-World Use

Durga Antivirus Pro applies a 100 req/min limit per IP for unauthenticated endpoints. This prevents basic abuse without requiring API keys. When a single IP exceeds the limit, the gateway returns 429, protecting backend resources from anonymous scraping.

flowchart LR
    Client["Client IP\n203.0.113.50"] --> Proxy["Proxy/CDN"]
    Proxy --> GW["Gateway\nRate Limiter"]
    GW -->|"Extract real IP\nfrom X-Forwarded-For"| RL["Per-IP Counter"]
    RL -->|"100 req/min"| Backend["Backend"]
    style GW fill:#dbeafe,stroke:#2563eb

Extracting Real Client IP

from flask import Flask, request

app = Flask(__name__)

def get_client_ip():
    # Try X-Forwarded-For (proxy/CDN)
    xff = request.headers.get("X-Forwarded-For")
    if xff:
        # Take the first IP in the chain (real client)
        return xff.split(",")[0].strip()

    # Try X-Real-IP (Nginx proxy)
    xri = request.headers.get("X-Real-IP")
    if xri:
        return xri

    # Fall back to remote address
    return request.remote_addr

IP Rate Limiter with Redis

import redis
import time

redis_client = redis.Redis(host="redis", port=6379, db=0)

class IPRateLimiter:
    def __init__(self, limit=100, window=60):
        self.limit = limit
        self.window = window

    def allow_request(self, client_ip):
        if ":" in client_ip:
            # IPv6: normalize and use as-is
            key = f"ip:{client_ip}"
        else:
            # IPv4: optionally group by /24
            prefix = ".".join(client_ip.split(".")[:3])
            key = f"ip:{prefix}.0/24"

        now = int(time.time())
        window_key = now - (now % self.window)
        counter_key = f"rl:{key}:{window_key}"

        count = redis_client.incr(counter_key)
        if count == 1:
            redis_client.expire(counter_key, self.window * 2)

        remaining = max(0, self.limit - count)
        reset = window_key + self.window

        if count > self.limit:
            return False, remaining, reset
        return True, remaining, reset

    def get_remaining(self, client_ip):
        prefix = ".".join(client_ip.split(".")[:3])
        key = f"ip:{prefix}.0/24"
        now = int(time.time())
        window_key = now - (now % self.window)
        count = int(redis_client.get(f"rl:{key}:{window_key}") or 0)
        return max(0, self.limit - count)

CIDR-Based IP Grouping

import ipaddress

def get_ip_group(client_ip, cidr_size=24):
    try:
        ip = ipaddress.ip_address(client_ip)
        if isinstance(ip, ipaddress.IPv4Address):
            network = ipaddress.ip_network(f"{client_ip}/{cidr_size}", strict=False)
            return str(network)
        else:
            # IPv6: use /64 by default
            network = ipaddress.ip_network(f"{client_ip}/64", strict=False)
            return str(network)
    except ValueError:
        return client_ip

# Group IPs by /24 for NAT networks
group = get_ip_group("203.0.113.50")
print(f"IP group: {group}")  # 203.0.113.0/24

group_v6 = get_ip_group("2001:db8::1")
print(f"IPv6 group: {group_v6}")  # 2001:db8::/64

IP Whitelist/Blacklist with Rate Limiting

WHITELISTED_IPS = set(["10.0.0.0/8", "192.168.0.0/16"])
BLACKLISTED_IPS = set(["1.2.3.4", "5.6.7.8"])

def check_ip_access(client_ip):
    ip = ipaddress.ip_address(client_ip)

    # Check blacklist first
    for blocked in BLACKLISTED_IPS:
        if ip == ipaddress.ip_address(blocked):
            return False, "IP blacklisted"

    # Check whitelist (bypass rate limiting)
    for wl_cidr in WHITELISTED_IPS:
        if ip in ipaddress.ip_network(wl_cidr):
            return True, "whitelisted"

    # Apply rate limiting
    return True, "rate_limited"

Common Mistakes

1. Using Untrusted X-Forwarded-For

Without validating X-Forwarded-For, a client can spoof their IP. Only trust the header if you control the proxy. Always validate input.

2. Not Handling IPv6

IPv4-only rate limiting leaves IPv6 traffic completely unrestricted. Always handle both address families.

3. Individual IP Limits Behind NAT

Companies with 1000 employees behind a single NAT IP will hit per-IP limits quickly. Use per-IP-group limits (/24 for IPv4, /64 for IPv6).

4. Rate Limiting Trusted Proxies

CDN and internal proxy IPs should be whitelisted. Rate-limit the CDN's IP, not your own infrastructure.

5. Not Normalizing IPv6 Addresses

The same IPv6 address can appear in different formats. Use ipaddress module to normalize before using as a key.

Practice Questions

  1. How do you extract the real client IP when behind a proxy?
  2. Why should IPv6 addresses be normalized before using as rate limit keys?
  3. What is the problem with per-IP limits behind NAT?
  4. How can clients spoof their IP in rate limiting?
  5. Why should trusted proxy IPs be whitelisted?

Answers:

  1. Use the first value in X-Forwarded-For if present, or X-Real-IP, falling back to request.remote_addr.
  2. IPv6 addresses have multiple representations (compressed, mixed case). Normalization ensures the same address always produces the same key.
  3. NAT means 1000 users share one IP. A single user's request can exhaust the limit for everyone else.
  4. A client can set a fake X-Forwarded-For header if the gateway trusts it without validation.
  5. Whitelisting proxy IPs prevents legitimate infrastructure traffic from being incorrectly rate limited.

Challenge: Design an IP-based rate limiting Strategy for a public API that handles: home users (single IP), corporate users (NAT /24), mobile users (changing IPs), and CDN traffic. Define limits and groupings for each.

FAQ

Should I rate limit by IP or by API key?

: Both. Use IP limits for anonymous traffic and API key limits for authenticated traffic. IP limits protect unauthenticated endpoints.

How do mobile apps handle IP rate limiting?

: Mobile IPs change frequently and are often shared (carrier NAT). Use per-user rate limiting for mobile apps, not per-IP.

What is the best CIDR size for IP grouping?

: /24 for IPv4 (256 addresses), /64 for IPv6. Adjust based on your traffic patterns and the typical network size of your users.

Can IP rate limiting prevent DDoS?

: It helps but is not sufficient. DDoS attacks use many IPs. Combine with rate limiting, WAF, and CDN-level DDoS protection.

How do I handle IP rate limiting in Kubernetes?

: Use the real client IP. Set externalTrafficPolicy: Local on the service to preserve the source IP.

Mini Project

Build a Flask middleware that extracts the real client IP, groups IPv4 by /24 and IPv6 by /64, applies per-group rate limits of 200 req/min, and adds whitelist support for known proxy IPs. Use Redis for distributed counters.

What's Next

Continue with User-Based Rate Limiting for authenticated clients, or explore API Key Rate Limiting for partner API management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro