TL;DR
Implement API rate limiting with a token bucket or sliding window algorithm. Return the standard IETF rate limit headers—RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset—and respond with 429 Too Many Requests when a client exceeds its quota. Modern PetstoreAPI applies per-user quotas and returns structured error responses.
Introduction
A client sends 10,000 requests to your API in one minute. Your database crashes, monitoring alerts fire, and other customers lose access. The cause could be an attack—or simply a buggy client stuck in a retry loop.
Rate limiting protects the API by controlling how many requests a client can make within a period. When the client exceeds the limit, return 429 Too Many Requests and tell them when to retry. This gives clients a chance to back off while keeping the service available.
The original Swagger Petstore does not implement rate limiting. Modern PetstoreAPI adds token bucket rate limiting, standard headers, per-user quotas, and structured error responses.
In this guide, you’ll learn how common rate limiting algorithms work, which headers to return, and how to test rate limit behavior with Modern PetstoreAPI and Apidog.
Why APIs Need Rate Limiting
Rate limiting protects your API from abuse and helps ensure fair usage.
Protection Against Abuse
Denial-of-service (DoS) attacks
An attacker can flood an endpoint with requests. Rate limiting caps the impact of that traffic.Credential stuffing
Attackers may try thousands of username and password combinations. Rate limiting slows these attempts down.Data scraping
Bots can scrape an entire dataset. Rate limiting makes large-scale scraping more difficult.Cost control
If your API calls expensive AI models or third-party services, rate limits help prevent runaway costs.
Fair Usage
Rate limits can:
- Prevent one client sending 1,000 requests per second from monopolizing resources
- Keep response times more predictable
- Enforce tiered access, such as 100 requests per hour for a free tier and 10,000 requests per hour for a paid tier
Operational Benefits
Rate limiting also supports:
- Capacity planning: define the maximum request load your API accepts
- Cost predictability: cap infrastructure and downstream service usage
- Graceful degradation: reduce the chance of cascading failures under load
Rate Limiting Algorithms
Each algorithm has different tradeoffs in accuracy, memory usage, and implementation complexity.
1. Fixed Window
A fixed window counts requests in discrete time intervals.
For example:
- Window 1,
00:00–00:59: 100 requests allowed - Window 2,
01:00–01:59: 100 requests allowed
A basic Redis implementation looks like this:
def is_allowed(user_id):
current_minute = get_current_minute()
key = f"rate_limit:{user_id}:{current_minute}"
count = redis.incr(key)
redis.expire(key, 60)
return count <= 100
Advantages:
- Simple to implement
- Low memory usage
Disadvantage:
- It permits boundary bursts. A client could send 100 requests at
00:59and another 100 at01:00, resulting in 200 requests within two seconds.
2. Sliding Window
A sliding window counts requests made during a rolling period.
At 01:30, a one-hour sliding window counts requests from 00:30 through 01:30.
def is_allowed(user_id):
now = time.time()
window_start = now - 3600
key = f"rate_limit:{user_id}"
# Remove requests outside the rolling window.
redis.zremrangebyscore(key, 0, window_start)
# Count requests still inside the window.
count = redis.zcard(key)
if count < 100:
redis.zadd(key, {now: now})
redis.expire(key, 3600)
return True
return False
Advantages:
- Avoids the fixed-window boundary burst
- Provides accurate rolling-window enforcement
Disadvantages:
- Uses more memory because it stores request timestamps
- Requires more implementation logic
In production, make the cleanup, count, and insert operation atomic so concurrent servers cannot approve requests based on the same stale count.
3. Token Bucket
A token bucket refills tokens at a fixed rate. Each accepted request consumes one token.
Example configuration:
- Bucket capacity: 100 tokens
- Refill rate: 10 tokens per second
- Cost per request: 1 token
def is_allowed(user_id):
now = time.time()
key = f"rate_limit:{user_id}"
# Read the current bucket state.
data = redis.hgetall(key)
tokens = float(data.get("tokens", 100))
last_refill = float(data.get("last_refill", now))
# Refill tokens based on elapsed time.
elapsed = now - last_refill
tokens = min(100, tokens + elapsed * 10)
if tokens >= 1:
tokens -= 1
redis.hset(key, "tokens", tokens)
redis.hset(key, "last_refill", now)
redis.expire(key, 3600)
return True
return False
Advantages:
- Allows short bursts up to the bucket capacity
- Smooths the request rate over time
- Is widely used for API rate limiting
Disadvantages:
- More complex than a fixed window
- Requires persistent per-client state
The read, refill, decrement, and write operations should be atomic in a distributed deployment.
4. Leaky Bucket
A leaky bucket places requests in a queue and processes them at a fixed rate.
Example configuration:
- Queue capacity: 100 requests
- Processing rate: 10 requests per second
Advantages:
- Produces a smooth output rate
- Helps protect downstream services
Disadvantages:
- Adds latency while requests wait in the queue
- Requires queue management
Which Algorithm Should You Use?
For many APIs, start with a token bucket. It supports reasonable bursts while smoothing sustained traffic.
Modern PetstoreAPI uses token bucket rate limiting with per-user quotas.
Standard Rate Limit Headers
Use the IETF standard rate limit headers described in draft-ietf-httpapi-ratelimit-headers.
Standard Headers
RateLimit-Limit reports the maximum number of requests allowed in the current limit window:
RateLimit-Limit: 100
RateLimit-Remaining reports how many requests remain:
RateLimit-Remaining: 45
RateLimit-Reset reports the number of seconds until the limit resets:
RateLimit-Reset: 3600
Example Response
GET /pets
200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 3600
{
"data": [
{}
]
}
Legacy Headers
Many APIs still expose non-standard headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1710331200
Prefer the standard RateLimit-* headers instead. The X- prefix is deprecated, and the legacy format is not standardized.
How Modern PetstoreAPI Implements Rate Limiting
Modern PetstoreAPI uses token bucket rate limiting and returns standard headers.
Rate Limits by Tier
Free tier:
- 100 requests per hour
- 1,000 requests per day
Pro tier:
- 10,000 requests per hour
- 100,000 requests per day
Enterprise tier:
- Custom limits
Successful Request
GET /v1/pets
200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 3540
{
"data": [
{}
]
}
Rate-Limit Exceeded Response
When the client exceeds the limit, return 429 Too Many Requests and include both rate limit headers and Retry-After:
GET /v1/pets
429 Too Many Requests
Content-Type: application/problem+json
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 120
Retry-After: 120
{
"type": "https://petstoreapi.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 100 requests per hour",
"instance": "/v1/pets",
"retryAfter": 120,
"limit": 100,
"window": "1h"
}
Per-User vs. Per-IP Rate Limits
For authenticated requests, rate limit by the user ID or API key:
user_id = get_authenticated_user()
is_allowed(user_id)
This is more accurate and fair than limiting every request from the same IP address together.
For unauthenticated requests, rate limit by the client IP address:
ip_address = request.remote_addr
is_allowed(ip_address)
IP-based limits are less accurate because multiple users may share an address, and VPNs can change the apparent source. They are still useful for public endpoints.
Modern PetstoreAPI uses per-user rate limiting for authenticated requests and per-IP rate limiting for public endpoints.
Rate Limit Response Format
When a client exceeds a limit, return 429 with an RFC 9457 problem-details response.
Response Structure
{
"type": "https://petstoreapi.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded your rate limit. Please try again later.",
"instance": "/v1/pets",
"retryAfter": 120,
"limit": 100,
"remaining": 0,
"reset": 120,
"window": "1h"
}
Headers
429 Too Many Requests
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 120
Retry-After: 120
Retry-After tells the client how many seconds to wait before retrying.
Testing Rate Limits with Apidog
Use Apidog to send repeated requests, inspect response headers, and validate the 429 response body.
Test Scenarios
1. Normal Usage
- Send 50 requests
- Confirm that all requests succeed
- Verify that
RateLimit-Remainingdecreases
2. Exceed the Limit
- Send 101 requests
- Confirm that the 101st request returns
429 - Verify the problem-details response
- Check the
Retry-Afterheader
3. Reset Behavior
- Exceed the limit
- Wait for the reset period
- Send another request
- Verify that the available quota is restored
4. Different Tiers
- Test the free tier with its 100-request hourly limit
- Test the pro tier with its 10,000-request hourly limit
- Verify that each tier enforces its configured quota
Apidog Test Example
// Test rate limit headers.
pm.test("Rate limit headers present", () => {
pm.response.to.have.header("RateLimit-Limit");
pm.response.to.have.header("RateLimit-Remaining");
pm.response.to.have.header("RateLimit-Reset");
});
// Test the rate-limit response.
pm.test("Returns 429 when limit exceeded", () => {
pm.response.to.have.status(429);
});
Run the request repeatedly through your test scenario or collection to trigger the quota. Then verify the headers and response body on the request that receives 429.
Rate Limiting Best Practices
Use standard headers
ReturnRateLimit-Limit,RateLimit-Remaining, andRateLimit-Resetinstead of customX-headers.Return
429, not403
429means “too many requests.”403means “forbidden.” Keep these responses distinct.Include
Retry-After
Tell clients when they can retry.Document your limits
Publish limits and reset behavior in your API documentation.Provide tiers
Use different limits for free, paid, and enterprise customers.Rate limit by user when possible
Per-user limits are generally more accurate and fair for authenticated requests.Allow reasonable bursts
A token bucket can absorb short bursts without penalizing normal usage.Monitor rate-limit hits
Track how often clients reach their limits. A high rate may indicate abusive traffic, a faulty client, or an undersized quota.Provide a rate-limit status endpoint
GET /v1/rate-limit
200 OK
{
"limit": 100,
"remaining": 45,
"reset": 3540
}
- Test before deployment Use Apidog to test normal usage, quota exhaustion, reset behavior, and tier-specific limits.
Rate limiting controls how frequently clients can access your endpoints. Authentication establishes which clients are permitted to access them in the first place. Setting up Better Auth for modern API authentication addresses that complementary layer.
Conclusion
Rate limiting protects your API from abuse, limits infrastructure costs, and helps ensure fair usage. For many APIs, a token bucket provides a practical balance between burst handling and sustained traffic control.
Return the standard IETF headers—RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset—and respond with 429 Too Many Requests plus an RFC 9457 problem-details body when a client exceeds its quota.
Modern PetstoreAPI demonstrates this approach with per-user quotas, standard headers, and structured error responses. Test your implementation with Apidog to verify headers, 429 responses, retry behavior, reset behavior, and edge cases before deployment.
FAQ
What rate limits should I set?
Start with conservative defaults, such as 100 requests per hour for a free tier and 10,000 requests per hour for a paid tier. Adjust them based on usage patterns and infrastructure capacity.
Should I rate limit by IP or user?
Use the user or API key for authenticated requests. Use IP-based limits for public endpoints where no user identity is available.
What happens if a client exceeds the rate limit?
Return 429 Too Many Requests with a Retry-After header. Do not permanently block the client; let them retry after the specified delay or reset period.
How do I handle rate limits for webhooks?
Webhooks are server-to-server requests, so they may need higher limits. Consider separate quotas for webhook traffic and regular API calls.
Should I rate limit internal services?
Yes, but configure higher limits for trusted internal services. Rate limiting can help prevent cascading failures within internal systems.
How do I test rate limiting?
Send repeated requests with Apidog and verify 429 responses, rate limit headers, response bodies, and reset behavior.
What if my API is behind a CDN?
CDN caching reduces origin load, but rate limiting is still needed for cache misses and for methods such as POST, PUT, and DELETE.
How do I implement rate limiting across multiple servers?
Use a shared data store such as Redis or Memcached to track rate limit state across servers. Local in-memory counters do not provide consistent enforcement in a distributed system.
Top comments (0)