- Home
- HTTP Headers
- X-RateLimit Headers
Header
X-RateLimit Headers
Learn how X-RateLimit headers inform API clients about rate limits, remaining requests, and reset times. Implement proper rate limiting in your applications.
TL;DR: Inform clients about API rate limits with headers like X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Helps clients manage usage and avoid hitting limits.
What is X-RateLimit?
The X-RateLimit headers (also known as RateLimit) inform clients about API rate limiting status. They’re like a gas gauge showing “You have 950 requests left out of 1000, and your tank refills in 1 hour.”
These headers help clients manage their API usage, implement backoff strategies, and avoid hitting rate limits.
How X-RateLimit Works
Client makes API request:
GET /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer token123
```http
**Server responds with rate limit info:**
```http
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1737216000
Content-Type: application/json
{"users": [...]}
Client knows:
- Limit: 1000 requests per hour
- Remaining: 950 requests left
- Reset: Limits reset at Unix timestamp 1737216000
Syntax
Common Header Names
X-RateLimit-Limit: <limit>
X-RateLimit-Remaining: <remaining>
X-RateLimit-Reset: <timestamp>
X-RateLimit-Retry-After: <seconds>
```http
### Alternative Names (Standardized)
```http
RateLimit-Limit: <limit>
RateLimit-Remaining: <remaining>
RateLimit-Reset: <timestamp>
Common Examples
Basic Rate Limiting
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1737216000
```text
1000 requests per window, 950 left, resets at timestamp.
### Rate Limit Exceeded
```http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1737216000
Retry-After: 3600
{"error": "Rate limit exceeded"}
Multiple Rate Limits
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1737216000
X-RateLimit-Limit-Second: 10
X-RateLimit-Remaining-Second: 8
```http
Per-hour and per-second limits.
### GitHub Style
```http
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1737216000
X-RateLimit-Used: 1
X-RateLimit-Resource: core
Real-World Scenarios
REST API Usage
GET /api/repos/user/project HTTP/1.1
Authorization: Bearer gh_token
HTTP/1.1 200 OK
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4950
X-RateLimit-Reset: 1737219600
X-RateLimit-Used: 50
X-RateLimit-Resource: core
{"name": "project", "stars": 1234}
```text
### Approaching Limit
```http
GET /api/data HTTP/1.1
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 10
X-RateLimit-Reset: 1737216000
Warning: 199 - "Approaching rate limit"
{"data": "..."}
Rate Limit Exceeded
GET /api/data HTTP/1.1
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1737216000
Retry-After: 3600
{
"error": "Rate limit exceeded",
"message": "Try again in 1 hour"
}
```http
### Per-User Rate Limiting
```http
GET /api/search HTTP/1.1
Authorization: Bearer user_token
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1737216000
X-RateLimit-User: user123
{"results": [...]}
Server Implementation
Express.js (Node.js)
const express = require('express')
const rateLimit = require('express-rate-limit')
const app = express()
// Using express-rate-limit middleware
const limiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 1000, // 1000 requests per hour
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: true, // Also return X-RateLimit headers
handler: (req, res) => {
res.status(429).json({
error: 'Too Many Requests',
message: 'Rate limit exceeded. Try again later.'
})
}
})
app.use('/api', limiter)
// Manual implementation
const rateLimitStore = new Map()
function rateLimit(req, res, next) {
const identifier = req.ip || req.headers['x-forwarded-for']
const limit = 1000
const windowMs = 60 * 60 * 1000 // 1 hour
const now = Date.now()
const windowStart = now - windowMs
// Get or create user's request history
let requests = rateLimitStore.get(identifier) || []
// Remove old requests outside the window
requests = requests.filter((timestamp) => timestamp > windowStart)
// Check if limit exceeded
if (requests.length >= limit) {
const oldestRequest = Math.min(...requests)
const resetTime = Math.ceil((oldestRequest + windowMs) / 1000)
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', 0)
res.setHeader('X-RateLimit-Reset', resetTime)
res.setHeader('Retry-After', Math.ceil((oldestRequest + windowMs - now) / 1000))
return res.status(429).json({
error: 'Rate limit exceeded'
})
}
// Add current request
requests.push(now)
rateLimitStore.set(identifier, requests)
// Calculate reset time
const resetTime = Math.ceil((now + windowMs) / 1000)
// Set rate limit headers
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', limit - requests.length)
res.setHeader('X-RateLimit-Reset', resetTime)
next()
}
app.use('/api', rateLimit)
```javascript
### Token Bucket Algorithm
```javascript
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity
this.tokens = capacity
this.refillRate = refillRate
this.lastRefill = Date.now()
}
refill() {
const now = Date.now()
const timePassed = now - this.lastRefill
const tokensToAdd = (timePassed / 1000) * this.refillRate
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd)
this.lastRefill = now
}
consume(tokens = 1) {
this.refill()
if (this.tokens >= tokens) {
this.tokens -= tokens
return true
}
return false
}
getRemaining() {
this.refill()
return Math.floor(this.tokens)
}
}
const buckets = new Map()
app.use('/api', (req, res, next) => {
const userId = req.user?.id || req.ip
const limit = 1000
// Get or create bucket for user
if (!buckets.has(userId)) {
buckets.set(userId, new TokenBucket(limit, limit / 3600)) // refill per second
}
const bucket = buckets.get(userId)
if (bucket.consume()) {
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', bucket.getRemaining())
res.setHeader('X-RateLimit-Reset', Math.ceil(Date.now() / 1000) + 3600)
next()
} else {
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', 0)
res.setHeader('Retry-After', 60)
res.status(429).json({
error: 'Rate limit exceeded'
})
}
})
FastAPI (Python)
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import time
from collections import defaultdict
app = FastAPI()
# Simple rate limiter
rate_limit_store = defaultdict(list)
RATE_LIMIT = 1000
WINDOW_SIZE = 3600 # 1 hour in seconds
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
# Get client identifier
client_id = request.client.host
now = time.time()
window_start = now - WINDOW_SIZE
# Get request history
requests = rate_limit_store[client_id]
# Remove old requests
requests = [req_time for req_time in requests if req_time > window_start]
# Check limit
if len(requests) >= RATE_LIMIT:
oldest_request = min(requests)
reset_time = int(oldest_request + WINDOW_SIZE)
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded"},
headers={
"X-RateLimit-Limit": str(RATE_LIMIT),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(reset_time),
"Retry-After": str(int(reset_time - now))
}
)
# Add current request
requests.append(now)
rate_limit_store[client_id] = requests
# Process request
response = await call_next(request)
# Add rate limit headers
reset_time = int(now + WINDOW_SIZE)
response.headers["X-RateLimit-Limit"] = str(RATE_LIMIT)
response.headers["X-RateLimit-Remaining"] = str(RATE_LIMIT - len(requests))
response.headers["X-RateLimit-Reset"] = str(reset_time)
return response
@app.get("/api/data")
async def get_data():
return {"data": "example"}
```javascript
### Redis-Based Rate Limiting
```javascript
const redis = require('redis')
const client = redis.createClient()
async function redisRateLimit(req, res, next) {
const userId = req.user?.id || req.ip
const key = `ratelimit:${userId}`
const limit = 1000
const window = 3600 // 1 hour
try {
// Increment counter
const requests = await client.incr(key)
// Set expiry on first request
if (requests === 1) {
await client.expire(key, window)
}
// Get TTL for reset time
const ttl = await client.ttl(key)
const resetTime = Math.ceil(Date.now() / 1000) + ttl
// Check limit
if (requests > limit) {
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', 0)
res.setHeader('X-RateLimit-Reset', resetTime)
res.setHeader('Retry-After', ttl)
return res.status(429).json({
error: 'Rate limit exceeded'
})
}
// Set headers
res.setHeader('X-RateLimit-Limit', limit)
res.setHeader('X-RateLimit-Remaining', limit - requests)
res.setHeader('X-RateLimit-Reset', resetTime)
next()
} catch (error) {
console.error('Rate limit error:', error)
next() // Fail open
}
}
app.use('/api', redisRateLimit)
Best Practices
For Servers
1. Always include rate limit headers
# ✅ Include on every response
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1737216000
# ❌ Don't only show when limit exceeded
```javascript
**2. Use clear, predictable limits**
```javascript
// ✅ Clear per-hour limit
const limits = {
free: 100,
basic: 1000,
premium: 10000
}
// ❌ Complex, hard-to-track limits
const limit = Math.random() * 1000
3. Provide helpful error messages
{
"error": "Rate limit exceeded",
"message": "You have made 1000 requests in the last hour. Limit resets at 2026-01-18T12:00:00Z",
"limit": 1000,
"remaining": 0,
"reset": 1737216000
}
```text
**4. Use Unix timestamps for reset time**
```javascript
// ✅ Unix timestamp (seconds)
res.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + 3600)
// ❌ ISO date string (harder to parse)
res.setHeader('X-RateLimit-Reset', new Date().toISOString())
5. Implement multiple rate limit tiers
const limits = {
perSecond: 10,
perMinute: 100,
perHour: 1000,
perDay: 10000
}
// Check all limits
checkRateLimit(user, 'second', limits.perSecond)
checkRateLimit(user, 'minute', limits.perMinute)
checkRateLimit(user, 'hour', limits.perHour)
```text
**6. Consider different limits for different endpoints**
```javascript
// Read operations
app.get('/api/data', rateLimit({ limit: 1000 }))
// Write operations (stricter)
app.post('/api/data', rateLimit({ limit: 100 }))
// Expensive operations (very strict)
app.post('/api/export', rateLimit({ limit: 10 }))
For Clients
1. Always check rate limit headers
async function apiCall(url) {
const response = await fetch(url)
const limit = parseInt(response.headers.get('X-RateLimit-Limit'))
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'))
const reset = parseInt(response.headers.get('X-RateLimit-Reset'))
console.log(`Rate limit: ${remaining}/${limit}, resets at ${new Date(reset * 1000)}`)
return response.json()
}
```javascript
**2. Implement exponential backoff**
```javascript
async function apiCallWithBackoff(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url)
if (response.status !== 429) {
return response.json()
}
// Rate limited - wait and retry
const retryAfter = parseInt(response.headers.get('Retry-After') || '60')
const backoff = Math.min(retryAfter * Math.pow(2, i), 300)
console.log(`Rate limited. Waiting ${backoff}s before retry ${i + 1}/${maxRetries}`)
await new Promise((resolve) => setTimeout(resolve, backoff * 1000))
}
throw new Error('Max retries exceeded')
}
3. Respect Retry-After header
async function handleRateLimit(response) {
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After'))
if (retryAfter) {
console.log(`Waiting ${retryAfter} seconds before retry`)
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000))
// Retry the request
return fetch(response.url)
}
}
return response
}
```javascript
**4. Monitor remaining requests**
```javascript
class APIClient {
constructor() {
this.rateLimitRemaining = null
this.rateLimitReset = null
}
async request(url) {
// Check if we're close to limit
if (this.rateLimitRemaining !== null && this.rateLimitRemaining < 10) {
const now = Date.now() / 1000
const waitTime = this.rateLimitReset - now
if (waitTime > 0) {
console.warn(`Low on rate limit. Waiting ${waitTime}s`)
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000))
}
}
const response = await fetch(url)
// Update rate limit info
this.rateLimitRemaining = parseInt(response.headers.get('X-RateLimit-Remaining'))
this.rateLimitReset = parseInt(response.headers.get('X-RateLimit-Reset'))
return response
}
}
Common Header Variations
Twitter/X Style
X-Rate-Limit-Limit: 180
X-Rate-Limit-Remaining: 179
X-Rate-Limit-Reset: 1737216000
```http
### GitHub Style
```http
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1737216000
X-RateLimit-Used: 1
X-RateLimit-Resource: core
Standardized (IETF Draft)
RateLimit-Limit: 1000
RateLimit-Remaining: 950
RateLimit-Reset: 3600
```http
### Stripe Style
```http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1737216000
Testing Rate Limits
Using curl
# Check rate limit headers
curl -I https://api.example.com/data
# Make multiple requests
for i in {1..10}; do
curl -s -I https://api.example.com/data | grep -i "x-ratelimit"
sleep 1
done
# Extract rate limit info
curl -s -I https://api.example.com/data | \
grep -E "X-RateLimit-(Limit|Remaining|Reset)"
```javascript
### Using JavaScript
```javascript
// Test rate limiting
async function testRateLimit() {
let requestCount = 0
while (true) {
requestCount++
const response = await fetch('/api/data')
const limit = response.headers.get('X-RateLimit-Limit')
const remaining = response.headers.get('X-RateLimit-Remaining')
const reset = response.headers.get('X-RateLimit-Reset')
console.log(`Request ${requestCount}: ${remaining}/${limit} remaining`)
if (response.status === 429) {
console.log('Rate limit hit!')
console.log('Reset at:', new Date(reset * 1000))
break
}
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
testRateLimit()
Related Headers
- Retry-After - When to retry after rate limit
- Warning - Additional warning information
- Date - Server time for calculating reset
- Age - Age of cached response (affects rate counting)
Standardized Rate Limit Headers (IETF Draft)
The IETF is standardizing rate limit headers under the names RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset (without the X- prefix). The draft specification also introduces RateLimit-Policy to describe the rate limiting policy in a machine-readable format. As this standard matures, new APIs should consider using the standardized names alongside the X-RateLimit-* variants for backward compatibility.
The key difference in the IETF draft is that RateLimit-Reset uses seconds until reset (relative time) rather than a Unix timestamp (absolute time). This avoids clock synchronization issues between client and server. When implementing rate limiting, emitting both formats during a transition period allows clients to adopt the standard at their own pace.
Frequently Asked Questions
What are X-RateLimit headers?
X-RateLimit headers communicate API rate limits to clients. Common headers are X-RateLimit-Limit (max requests), X-RateLimit-Remaining (requests left), X-RateLimit-Reset (when limit resets).
How do I handle rate limits?
Check X-RateLimit-Remaining before requests. When low, slow down. If you get 429, wait until X-RateLimit-Reset time. Implement exponential backoff for retries.
Are X-RateLimit headers standardized?
Not yet, but IETF is working on RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset standards. Currently, different APIs use slightly different header names.
What does X-RateLimit-Reset contain?
Usually a Unix timestamp when the limit resets. Some APIs use seconds until reset. Check API documentation for the specific format used.