How HTTP Works

Response header · non-standard

< X-RateLimit-Remaining: 42

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.

Reviewed 9 min readintermediate2 sourcesTry itMarkdown
Direction
Response
Category
Rate limiting
Status
Non-standard: X-RateLimit-* is a vendor convention; an IETF draft defines RateLimit fields
On this page

TL;DR: X-RateLimit-Limit, -Remaining and -Reset tell API clients their quota, but they are a convention, not a standard: names and reset units (Unix timestamp or seconds) differ per API. The IETF draft replaces them with RateLimit and RateLimit-Policy.

What is X-RateLimit?

The X-RateLimit headers 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

Server responds with rate limit info:

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>

Alternative Names (Standardized)

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

1000 requests per window, 950 left, resets at timestamp.

Rate Limit Exceeded

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

Per-hour and per-second limits.

GitHub Style

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}

Approaching Limit

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"
}

Per-User Rate Limiting

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
  limit: 1000, // v7+ option name (formerly `max`): 1000 requests per hour
  standardHeaders: 'draft-7', // RateLimit and RateLimit-Policy headers
  legacyHeaders: true, // Also send the 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)

Token Bucket Algorithm

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"}

Redis-Based Rate Limiting

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)

Getting X-RateLimit Headers right

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

2. Use clear, predictable limits

// ✅ 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
}

4. Use Unix timestamps for reset time

// ✅ 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)

6. Consider different limits for different endpoints

// 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()
}

2. Implement exponential backoff

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
}

4. Monitor remaining requests

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

GitHub Style

X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1737216000
X-RateLimit-Used: 1
X-RateLimit-Resource: core

IETF draft (older revisions)

RateLimit-Limit: 1000
RateLimit-Remaining: 950
RateLimit-Reset: 3600

Newer revisions replace these three with RateLimit and RateLimit-Policy; see the end of this page.

Stripe Style

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)"

Using 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()

The IETF draft: RateLimit and RateLimit-Policy

X-RateLimit-* is a convention, not a standard. The IETF HTTPAPI working group’s draft (draft-ietf-httpapi-ratelimit-headers, at revision 11 as of this review and still an Internet-Draft, not an RFC) defines two Structured Field headers instead of three separate ones:

RateLimit-Policy: "default";q=100;w=10
RateLimit: "default";r=50;t=30

Read that as: the policy named default allows 100 quota units (q) per 10-second window (w); 50 remain (r) and the window resets in 30 seconds (t). Earlier drafts used RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, and some libraries (including express-rate-limit with standardHeaders: 'draft-6') still emit those. Reset is a relative number of seconds, not a Unix timestamp, which avoids clock-sync problems. Because the draft has changed shape between revisions, check which one a given API implements rather than assuming.

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

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?

No. The IETF is standardising RateLimit and RateLimit-Policy header fields in an Internet-Draft that is not yet an RFC; earlier drafts used RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. In practice each API uses its own X-RateLimit-* names and units.

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.

Sources

  1. IETF Rate Limit Headers Draftdatatracker.ietf.org
  2. GitHub API Rate Limitingdocs.github.com

Keep going

Browse /search