HTTP

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.

8 min read intermediate Try in Playground

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()
  • 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.

Keep Learning