Introduction
Rate limiting is a defensive control that restricts how many requests a client can make to an API within a given time window. It protects services from abuse, accidental overload, and denial-of-service attacks while preserving quality of service for legitimate users.
For a penetration tester or red-team operator, identifying the exact limits and where they are enforced is a critical reconnaissance step. Knowing the limits lets you craft payloads that stay under the radar, or deliberately trigger limits to cause service degradation.
Real-world examples include Twitter’s 180 requests / 15 minutes per user, Stripe’s 100 requests / second per account, and many internal micro-services that silently drop traffic once a burst threshold is crossed.
Prerequisites
- Fundamentals of HTTP/HTTPS (methods, status codes, headers)
- Basic API interaction patterns (REST, GraphQL, pagination)
- Comfortable with a command line and at least one scripting language (Python, Bash)
Core Concepts
Before you can spot a rate-limit, you need to understand the algorithms that most services implement.
Fixed Window
The simplest form: a counter resets at the start of each discrete interval (e.g., every minute). If the counter exceeds the quota, the server returns 429 Too Many Requests until the window rolls over.
Sliding Window
Instead of a hard reset, the window “slides” with each request. The server looks at the number of requests in the past n seconds. This approach smooths bursts and is harder to game.
Token Bucket
A bucket holds a number of tokens that replenish at a steady rate. Each request consumes a token; if the bucket is empty, the request is rejected. Token buckets support bursts (when the bucket is full) while still enforcing an average rate.
Figure 1 (described): A timeline showing three windows - Fixed (hard reset at 00:00), Sliding (continuous 60-second window), Token Bucket (tokens added each second, burst capacity shown as bucket size).
Understanding which algorithm is in play helps you predict how quickly the limit will recover after a 429 response.
Understanding common rate-limit mechanisms (fixed window, sliding window, token bucket)
Most modern APIs expose the algorithm indirectly via response headers. For example, a X-RateLimit-Limit of 1000 combined with a Retry-After: 60 header suggests a fixed-window of 1,000 requests per minute.
When headers are missing, you can infer the algorithm by sending controlled bursts and observing the pattern of 429 responses. A sudden drop after the first n requests followed by a clean slate after a fixed time indicates a fixed window. A gradual increase in Retry-After values points to a sliding window.
import time, requests
url = "https://api.example.com/v1/items"
for i in range(120): r = requests.get(url) print(i, r.status_code, r.headers.get('X-RateLimit-Remaining')) time.sleep(0.2) # 5 req/sec
The script above lets you plot remaining quota over time and visually spot the shape of the window.
Locating rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After)
Headers are the most reliable source of rate-limit data. Vendors differ in naming conventions, but the three most common are:
X-RateLimit-Limit- total allowed requests in the current window.X-RateLimit-Remaining- how many requests are left before the limit is hit.Retry-After- seconds (or HTTP-date) the client should wait before retrying.
Some APIs also expose a RateLimit-Reset timestamp (UNIX epoch) that tells you exactly when the counter will roll over.
Example of a successful response with limits:
HTTP/1.1 200 OK
Date: Wed, 26 Jul 2026 12:00:00 GMT
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4987
X-RateLimit-Reset: 1690368000
Content-Type: application/json
{...}
When the limit is exceeded:
HTTP/1.1 429 Too Many Requests
Date: Wed, 26 Jul 2026 12:00:10 GMT
Retry-After: 30
Content-Type: application/json
{ "error": "Rate limit exceeded" }
Note that some services hide these headers behind a CDN or WAF; you may need to disable compression or use a different User-Agent to see them.
Analyzing response codes (429 Too Many Requests) and error messages
The HTTP status 429 is the canonical indicator of a rate limit breach. However, not every service follows the spec. Some return 403 Forbidden with a message like “quota exceeded”, while others embed the error in a 200 body.
Key things to record when you encounter a limit:
- Exact status code (429, 403, 503, etc.)
- Response body - does it contain a JSON field
errorormessage? - Headers - especially
Retry-Afterand any custom limit headers. - Timing - how long after the request did the server respond?
Collecting this data across multiple endpoints helps you map whether limits are per-endpoint, per-IP, per-API-key, or global.
Fingerprinting rate-limit enforcement points (gateway, WAF, application code)
Rate limiting can be enforced at several layers:
- API Gateway (e.g., Kong, Amazon API Gateway) - usually adds standard headers and returns
429early, before request reaches the backend. - Web Application Firewall (WAF) - may inject its own headers (e.g.,
Server: cloudflare) and sometimes returns a generic HTML error page. - Application code - custom logic inside the service, often returning JSON-formatted error messages and bespoke header names.
To differentiate, observe these clues:
| Signal | Likely Enforcement Point |
|---|---|
Consistent Server: nginx with X-RateLimit-* headers | Gateway or reverse proxy |
| HTML error page with Cloudflare branding | WAF (Cloudflare, Akamai) |
JSON body containing business-specific fields (e.g., error_code: USER_QUOTA_EXCEEDED) | Application code |
Another technique is to vary the source IP (via proxies or VPNs). If the limit resets per IP, the enforcement is likely at the edge (gateway/WAF). If the limit follows the API key regardless of IP, it is probably inside the application.
Using tools (curl, httpie, Burp Suite) to enumerate limits
Below are practical command-line recipes that let you systematically probe an API.
curl
# Simple request showing all headers
curl -i -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/orders
# Loop 120 times, 1 request per second, logging status and limit headers
for i in $(seq 1 120); do curl -s -o /dev/null -w "%{http_code} %{url_effective} X-RateLimit-Remaining:%{header_x-ratelimit-remaining} Retry-After:%{header_retry-after}
" -H "Authorization: Bearer $TOKEN" https://api.example.com/v1/orders sleep 1
done
httpie
http GET https://api.example.com/v1/products "Authorization:Bearer $TOKEN" --print=h
# Using httpie in a Python one-liner for rapid burst testing
python - <<'PY'
import subprocess, time
url = 'https://api.example.com/v1/products'
for i in range(50): r = subprocess.run(['http', '--print=h', url, 'Authorization:Bearer $TOKEN'], capture_output=True, text=True) print(i, r.stdout.strip()) time.sleep(0.1)
PY
Burp Suite
Burp’s Intruder can be configured to send a high-frequency payload list (e.g., 1,2,3,…,200) while capturing the Response tab. Use the Match & Replace rule to insert a unique header per request, then sort the results by HTTP code and Response time. Burp’s Extension: BApp Store → Rate Limiter automatically extracts X-RateLimit-* headers and visualises the remaining quota.
For automated enumeration, the Turbo Intruder extension allows you to script the attack in JavaScript.
function queueRequests(base) { for (let i = 0; i < 200; i++) { let req = base.clone(); req.setHeader('X-Seq', i); send(req); }
}
Note the escaped < and > in the JavaScript example, as required for JSON encoding.
Practical Examples
Let’s walk through a realistic scenario: discovering the rate limits of a public “books” API that requires an API key.
- Step 1 - Baseline request: Use
curlto issue a single request and capture headers.curl -i -H "X-API-Key: abc123" https://api.books.example.com/v1/search?q=python - Step 2 - Burst test: Send 30 requests in rapid succession and log
429occurrences.for i in $(seq 1 30); do curl -s -o /dev/null -w "%{http_code} %{header_retry-after} " -H "X-API-Key: abc123" https://api.books.example.com/v1/search?q=python; done - Step 3 - Analyse pattern: If you see
429after the 20th request andRetry-After: 60, you likely have a fixed window of 20 requests per minute. - Step 4 - Verify per-key vs per-IP: Change the
X-API-Keyvalue while keeping the same source IP. If the new key gets a fresh quota, the limit is per-key. - Step 5 - Document: Record the discovered limits, enforcement point (gateway header present), and any quirks (e.g., missing
Retry-Afteron the first breach).
Repeating this process across multiple endpoints (search, details, reviews) reveals whether the API applies a global quota or per-resource quotas.
Tools & Commands
- curl - lightweight, scriptable, works on any OS.
curl -I -H "Authorization: Bearer $TOKEN" $ENDPOINT - httpie - human-friendly output, automatic JSON pretty-print.
http GET $ENDPOINT Authorization:"Bearer $TOKEN" - Burp Suite (Intruder / Turbo Intruder) - visual analysis, can capture timing, supports extensions.
- Python script - for fine-grained timing and statistical analysis.
import time, requests, statistics url = "https://api.example.com/v1/resource" headers = {"Authorization": "Bearer abc123"} remaining = [] for i in range(150): start = time.time() r = requests.get(url, headers=headers) elapsed = time.time() - start remaining.append(int(r.headers.get('X-RateLimit-Remaining', -1))) print(i, r.status_code, remaining[-1], f"{elapsed:.3f}s") time.sleep(0.2) print('Mean remaining:', statistics.mean([x for x in remaining if x!=-1]))
Defense & Mitigation
From the defender’s perspective, rate limiting is a first-line mitigation against credential stuffing, scraping, and DDoS. Best practices include:
- Implement a token-bucket algorithm at the edge (gateway) to allow short bursts while enforcing an average rate.
- Publish clear
X-RateLimit-*headers so legitimate clients can self-throttle. - Return
429with aRetry-Afterheader; avoid generic 5xx codes that hide the intent. - Couple rate limits with IP reputation and behavioural analytics - e.g., increase penalty for repeated violations.
- Log every limit breach with user identifier, IP, endpoint, and timestamp for forensic analysis.
When designing micro-service architectures, enforce limits close to the public surface (API gateway) and optionally repeat enforcement inside the service for defense-in-depth.
Common Mistakes
- Assuming a single global limit - many APIs have per-endpoint or per-method limits. Test each major route.
- Ignoring
Retry-Afterunits - some services return a HTTP-date instead of seconds; mis-parsing leads to premature retries. - Over-relying on status code - a 200 response may still contain an error field indicating quota exhaustion.
- Not rotating API keys or IPs - testers often hit the same key, causing early lockout and missing broader limits.
- Failing to reset state between runs - some services keep a per-client sliding window that persists across test sessions.
Real-World Impact
Failure to respect rate limits can cripple both attackers and defenders. In 2022, a mis-configured token bucket on a fintech API allowed an attacker to send 10 k requests per second for 30 seconds before the bucket emptied, leading to a temporary outage for legitimate users.
Conversely, overly aggressive limits can break legitimate integrations. A popular SaaS platform reduced its per-minute quota from 1,000 to 100 after a DoS incident, causing dozens of partner applications to fail.
My experience shows that most organizations expose rate-limit headers but hide the exact algorithm. By combining header analysis with timing curves, you can reliably infer the model and predict recovery times - a valuable edge in both red-team engagements and bug-bounty hunting.
Practice Exercises
- Exercise 1 - Header Harvesting: Choose a public API (e.g., GitHub, OpenWeather) and capture all rate-limit related headers for three different endpoints. Record the values in a spreadsheet.
- Exercise 2 - Algorithm Inference: Write a Python script that sends 200 requests at a constant 5 req/sec to a chosen endpoint. Plot
Remainingvs time and annotate whether the curve matches fixed, sliding, or token-bucket behavior. - Exercise 3 - Enforcement Point Fingerprinting: Using Burp Suite, send the same request from two different IPs (via VPN). Compare the response headers and bodies to decide whether the limit is enforced at the edge or inside the application.
- Exercise 4 - Bypass Exploration: After discovering a 60-second fixed window, experiment with
Retry-Aftermanipulation (e.g., sending a request with a customRetry-Afterheader). Observe if the server respects it or overrides it.
Document each step, the commands you used, and the conclusions you drew.
Further Reading
- RFC 6585 - "Additional HTTP Status Codes" (defines 429)
- OWASP API Security Top 10 - API4: Lack of Resources & Rate Limiting
- “Designing Scalable APIs with Token Bucket” - NGINX blog post (2023)
- Burp Suite BApp: RateLimiter
- “GraphQL Rate Limiting Strategies” - GraphQL Foundation whitepaper (2022)
Summary
Identifying rate-limiting controls is a blend of HTTP header analysis, status-code interpretation, and timing experiments. By mastering fixed window, sliding window, and token-bucket models, you can accurately fingerprint where limits are enforced (gateway, WAF, or application) and adapt your testing strategy accordingly. Use the presented tools-curl, httpie, Burp Suite, and custom scripts-to enumerate limits systematically, avoid common pitfalls, and provide actionable recommendations for both attackers and defenders.