Every API that matters will rate-limit you — and how you respond to rate limits determines whether your integration is reliable or not
Rate limiting is protective infrastructure: it prevents any single client from monopolising a shared resource. For developers consuming APIs, rate limits are a constraint to design around. For developers building APIs, rate limiting is a requirement that affects architecture. Understanding the four common rate limiting algorithms and how to implement well-behaved clients around them makes the difference between integrations that handle traffic gracefully and ones that fail under load.
Four rate limiting algorithms
1. Fixed window counter
The simplest approach: count requests in a fixed time window (e.g., the current minute). Reset the counter at the start of each new window.
Window: 12:00:00 → 12:00:59
Limit: 100 requests per minute
Counter: increments with each request; resets at 12:01:00
The weakness: a burst attack — 100 requests at 12:00:59 and 100 requests at 12:01:00 — sends 200 requests in 2 seconds while technically staying within the limit. The boundary between windows creates a 2× burst opportunity.
2. Sliding window log
Tracks the timestamp of every request in a log. For each new request, count how many requests occurred in the past N seconds. If under the limit, allow; otherwise, reject.
Accurate but memory-intensive: every request creates a log entry. For high-throughput APIs, this log can become very large.
3. Sliding window counter (hybrid)
Approximates the sliding window by combining two fixed window counters:
Current window requests: 40 (at 70% through the window)
Previous window requests: 60
Weighted estimate = 60 × (1 - 0.70) + 40 = 58 estimated requests in rolling window
Good balance of accuracy and memory efficiency. Used by Redis-based rate limiters like redis-cell.
4. Token bucket
A bucket holds up to N tokens. Tokens are added at a constant rate (e.g., 10 per second). Each request consumes one token. If the bucket is empty, the request is rejected.
The key property: allows short bursts (up to the bucket capacity) while enforcing a sustained rate limit. This is the most common algorithm for APIs where brief traffic spikes are acceptable but sustained overloading is not.
Bucket capacity: 100 tokens
Refill rate: 10 tokens/second
Initial state: 100 tokens
Burst of 50 requests: bucket goes from 100 → 50 tokens (allowed)
50 requests/second sustained: exceeds refill rate of 10/sec → bucket drains → requests rejected
Leaky bucket is the inverse: requests enter a queue at any rate and exit at a fixed rate. Smooths traffic but adds latency (requests wait in the queue).
Reading rate limit response headers
Well-designed APIs communicate rate limit status through response headers. The emerging standard (RFC 6585 / IETF draft):
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1731682800
Retry-After: 3600
Common header names by API:
| Header | Used by | Meaning |
|---|---|---|
X-RateLimit-Limit |
GitHub, Twitter/X, most REST APIs | Maximum requests per window |
X-RateLimit-Remaining |
GitHub, Twitter/X | Remaining requests in current window |
X-RateLimit-Reset |
GitHub | Unix timestamp when window resets |
X-RateLimit-Reset-After |
Discord | Seconds until reset |
Retry-After |
Standard (RFC 7231) | Seconds to wait before retrying |
RateLimit-Policy |
Emerging standard | Machine-readable policy description |
GitHub example (using curl -I):
X-RateLimit-Limit: 5000
X-RateLimit-Used: 4923
X-RateLimit-Remaining: 77
X-RateLimit-Reset: 1731683100
If Remaining is 77 and Reset is in 8 minutes, you have 77 requests over 8 minutes — spread them or pause.
Exponential backoff with jitter
When rate-limited (HTTP 429) or encountering transient errors (HTTP 503), the standard retry pattern is exponential backoff with jitter:
import time
import random
def api_request_with_backoff(url, max_retries=5):
for attempt in range(max_retries):
response = requests.get(url)
if response.status_code == 200:
return response
if response.status_code == 429:
# Check Retry-After header first
retry_after = response.headers.get('Retry-After')
if retry_after:
time.sleep(int(retry_after))
continue
# Exponential backoff: 1s, 2s, 4s, 8s, 16s
base_delay = 2 ** attempt
# Jitter: add randomness to prevent thundering herd
jitter = random.uniform(0, base_delay * 0.1)
delay = base_delay + jitter
time.sleep(delay)
elif response.status_code >= 500:
# Server error — also back off
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
else:
# Client error (4xx other than 429) — don't retry
raise APIError(f"Client error: {response.status_code}")
raise MaxRetriesExceeded("Failed after max retries")
Why jitter matters: without jitter, multiple clients hitting a rate limit simultaneously will all retry at the same interval — the "thundering herd" — repeatedly hitting the rate limit together. Jitter spreads retry timing, reducing the likelihood of synchronised overload.
Client-side rate limiting (proactive)
Rather than hitting the rate limit and backing off, proactive client-side rate limiting respects the known rate limit before making requests:
import time
from collections import deque
class RateLimitedClient:
def __init__(self, requests_per_second):
self.rate = requests_per_second
self.timestamps = deque()
def wait_if_needed(self):
now = time.time()
window_start = now - 1.0 # 1-second window
# Remove timestamps outside the window
while self.timestamps and self.timestamps[0] < window_start:
self.timestamps.popleft()
# If at the rate limit, wait until oldest request expires
if len(self.timestamps) >= self.rate:
sleep_time = self.timestamps[0] - window_start
time.sleep(sleep_time)
self.timestamps.append(time.time())
client = RateLimitedClient(requests_per_second=10)
for item in large_dataset:
client.wait_if_needed()
result = api.get(item)
Distributed rate limiting considerations
In multi-instance deployments (load-balanced API consumers), each instance has its own in-memory rate limit state. Instance A might think it has 50 remaining requests while Instance B also thinks it has 50 — both proceed and collectively exceed the limit.
Solutions:
- Redis-based distributed counter: all instances share a single counter in Redis. Atomic increment with TTL ensures consistent counting across instances.
- Per-instance quota allocation: divide the total rate limit by the number of instances. If the API allows 1,000 requests/minute and you have 5 instances, each instance treats 200 requests/minute as its limit.
- Centralised request queue: all API calls go through a single queue with a dedicated rate-limited consumer.
How to use the REST API Checker on sadiqbd.com
- Test any endpoint — send GET, POST, PUT, DELETE requests directly from your browser
- Inspect response headers — check rate limit headers (
X-RateLimit-Remaining,Retry-After) to understand the API's limits before building an integration - Verify error responses — trigger a 429 response deliberately to see what headers the API returns
- Test authentication — add
Authorization: Bearer <token>headers and verify the API accepts them
Frequently Asked Questions
What HTTP status code should a rate-limited response return?
HTTP 429 (Too Many Requests), defined in RFC 6585. The response should include a Retry-After header indicating when the client may retry. Some APIs use 503 (Service Unavailable) for rate limiting, which is less precise — 503 implies a server problem rather than a client-side rate excess.
How do I test my integration against rate limits without deploying? Use a mock API server (WireMock, Postman Mock Server, or a simple Express endpoint) that responds with 429 after N requests, with appropriate headers. Test your retry logic against this before hitting the real API.
Is the REST API Checker free? Yes — completely free, no sign-up required.
Try the REST API Checker free at sadiqbd.com — send requests to any REST API, inspect all response headers including rate limit headers, and test authentication from your browser.