429 Too Many Requests

Fundamentals Security Notes Jan 9, 2026 HTTP
http status-codes rate-limiting security rest

Definition

429 Too Many Requests is an HTTP status code indicating that the client has sent too many requests in a given time period. This is part of rate limiting, a critical security and performance mechanism to prevent abuse, DoS attacks, and resource exhaustion.

The response should include:

  1. Retry-After Header - When the client can retry (seconds or HTTP date)
  2. Rate Limit Info - Current limits and reset time (e.g., X-RateLimit-* headers)
  3. Error Details - Explanation of the rate limit policy

Common rate limit strategies:

  • Fixed Window - 100 requests per hour (resets at the top of each hour)
  • Sliding Window - 100 requests per rolling 60-minute window
  • Token Bucket - Requests consume tokens; tokens refill at a constant rate
  • Leaky Bucket - Requests queued and processed at a constant rate

429 is defined in RFC 6585 and is essential for protecting APIs from abuse and ensuring fair resource usage.

Example

429 Too Many Requests - Rate Limit Exceeded:

POST /api/users HTTP/1.1
Host: api.example.com
Authorization: Bearer YOUR_TOKEN

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1736424000
Content-Type: application/json

{
  "error": "Too Many Requests",
  "message": "Rate limit exceeded. You can make 100 requests per hour.",
  "limit": 100,
  "remaining": 0,
  "resetAt": "2026-01-09T12:00:00Z",
  "retryAfter": 3600
}

429 with HTTP Date in Retry-After:

GET /api/search?q=example HTTP/1.1
Host: api.example.com

HTTP/1.1 429 Too Many Requests
Retry-After: Thu, 09 Jan 2026 12:00:00 GMT
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1736424000
Content-Type: application/json

{
  "error": "Too Many Requests",
  "message": "Search rate limit exceeded. Try again after 2026-01-09T12:00:00Z.",
  "limit": 10,
  "remaining": 0,
  "resetAt": "2026-01-09T12:00:00Z"
}

Code Example

JavaScript (Fetch API with Retry Logic):

const apiRequest = async (url, options = {}, maxRetries = 3) => {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(url, {
        ...options,
        headers: {
          'Authorization': 'Bearer YOUR_TOKEN',
          'Accept': 'application/json',
          ...options.headers
        }
      });
      
      // Handle 429 Too Many Requests
      if (response.status === 429) {
        const error = await response.json();
        console.warn('429 Too Many Requests:', error.message);
        
        // Get retry timing from headers
        const retryAfter = response.headers.get('Retry-After');
        const ratelimitReset = response.headers.get('X-RateLimit-Reset');
        
        let delayMs;
        
        // Retry-After can be seconds or HTTP date
        if (retryAfter) {
          const retryAfterInt = parseInt(retryAfter);
          
          if (isNaN(retryAfterInt)) {
            // HTTP date format
            const resetDate = new Date(retryAfter);
            delayMs = resetDate.getTime() - Date.now();
          } else {
            // Seconds
            delayMs = retryAfterInt * 1000;
          }
        } else if (ratelimitReset) {
          // Unix timestamp
          const resetDate = new Date(parseInt(ratelimitReset) * 1000);
          delayMs = resetDate.getTime() - Date.now();
        } else {
          // Fallback: exponential backoff
          delayMs = Math.min(1000 * Math.pow(2, attempt), 60000);
        }
        
        console.log(`Waiting ${delayMs}ms before retry (attempt ${attempt + 1}/${maxRetries})...`);
        
        // Wait before retrying
        await new Promise(resolve => setTimeout(resolve, delayMs));
        continue; // Retry
      }
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return await response.json();
      
    } catch (error) {
      if (attempt === maxRetries - 1) {
        console.error('Max retries reached:', error);
        throw error;
      }
      
      console.error(`Attempt ${attempt + 1} failed:`, error.message);
    }
  }
};

// Example: Creating user with rate limit handling
const createUser = async (userData) => {
  try {
    const result = await apiRequest('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(userData)
    });
    
    console.log('User created:', result);
    return result;
    
  } catch (error) {
    console.error('Failed to create user:', error);
    throw error;
  }
};

// Example: Checking rate limit headers before request
const checkRateLimit = async () => {
  const response = await fetch('https://api.example.com/rate-limit', {
    headers: {
      'Authorization': 'Bearer YOUR_TOKEN'
    }
  });
  
  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('Rate Limit:', {
    limit,
    remaining,
    resetAt: new Date(parseInt(reset) * 1000).toISOString()
  });
  
  return {
    limit: parseInt(limit),
    remaining: parseInt(remaining),
    resetAt: new Date(parseInt(reset) * 1000)
  };
};

Python (requests library with Retry Logic):

import requests
import time
from datetime import datetime

def api_request(url, method='GET', json=None, max_retries=3):
    headers = {
        'Authorization': 'Bearer YOUR_TOKEN',
        'Accept': 'application/json'
    }
    
    for attempt in range(max_retries):
        try:
            response = requests.request(
                method,
                url,
                json=json,
                headers=headers
            )
            
            # Handle 429 Too Many Requests
            if response.status_code == 429:
                error = response.json()
                print(f'429 Too Many Requests: {error.get("message")}')
                
                # Get retry timing from headers
                retry_after = response.headers.get('Retry-After')
                ratelimit_reset = response.headers.get('X-RateLimit-Reset')
                
                delay_seconds = None
                
                # Retry-After can be seconds or HTTP date
                if retry_after:
                    try:
                        # Try parsing as integer (seconds)
                        delay_seconds = int(retry_after)
                    except ValueError:
                        # Parse as HTTP date
                        reset_date = datetime.strptime(retry_after, '%a, %d %b %Y %H:%M:%S %Z')
                        delay_seconds = (reset_date - datetime.now()).total_seconds()
                
                elif ratelimit_reset:
                    # Unix timestamp
                    reset_date = datetime.fromtimestamp(int(ratelimit_reset))
                    delay_seconds = (reset_date - datetime.now()).total_seconds()
                
                else:
                    # Fallback: exponential backoff
                    delay_seconds = min(2 ** attempt, 60)
                
                print(f'Waiting {delay_seconds}s before retry (attempt {attempt + 1}/{max_retries})...')
                
                # Wait before retrying
                time.sleep(max(delay_seconds, 0))
                continue  # Retry
            
            response.raise_for_status()
            
            return response.json()
            
        except requests.exceptions.RequestException as e:
            if attempt == max_retries - 1:
                print(f'Max retries reached: {e}')
                raise
            
            print(f'Attempt {attempt + 1} failed: {e}')

# Example: Creating user with rate limit handling
def create_user(user_data):
    try:
        result = api_request(
            'https://api.example.com/users',
            method='POST',
            json=user_data
        )
        
        print(f'User created: {result}')
        return result
        
    except Exception as error:
        print(f'Failed to create user: {error}')
        raise

# Example: Checking rate limit headers before request
def check_rate_limit():
    response = requests.get(
        'https://api.example.com/rate-limit',
        headers={'Authorization': 'Bearer YOUR_TOKEN'}
    )
    
    limit = response.headers.get('X-RateLimit-Limit')
    remaining = response.headers.get('X-RateLimit-Remaining')
    reset = response.headers.get('X-RateLimit-Reset')
    
    print('Rate Limit:', {
        'limit': limit,
        'remaining': remaining,
        'resetAt': datetime.fromtimestamp(int(reset)).isoformat()
    })
    
    return {
        'limit': int(limit),
        'remaining': int(remaining),
        'resetAt': datetime.fromtimestamp(int(reset))
    }

Diagram

sequenceDiagram
    participant Client
    participant API
    participant RateLimiter
    
    Note over Client,RateLimiter: Initial Requests
    Client->>API: Request 1
    API->>RateLimiter: Check limit
    RateLimiter-->>API: OK (99 remaining)
    API->>Client: 200 OK
X-RateLimit-Remaining: 99 Client->>API: Request 2 API->>RateLimiter: Check limit RateLimiter-->>API: OK (98 remaining) API->>Client: 200 OK
X-RateLimit-Remaining: 98 Note over Client,RateLimiter: ... 97 more requests ... Client->>API: Request 101 API->>RateLimiter: Check limit RateLimiter-->>API: LIMIT EXCEEDED API->>Client: 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Remaining: 0 Note over Client: Wait for Retry-After Client->>Client: Sleep 3600s Note over RateLimiter: Rate limit window resets Client->>API: Request (after reset) API->>RateLimiter: Check limit RateLimiter-->>API: OK (99 remaining) API->>Client: 200 OK
X-RateLimit-Remaining: 99

Security Notes

SECURITY NOTES
CRITICAL - Implement rate limiting to prevent DoS attacks and brute force attempts. Always include Retry-After header to prevent clients from hammering the API. Use different rate limits for authenticated vs unauthenticated requests. Track rate limits per user/IP/API key not globally. Log rate limit violations for security monitoring. Set aggressive limits on authentication endpoints to prevent credential stuffing. Use 429 instead of 503 for rate limiting to distinguish from server issues. Implement distributed rate limiting in multi-server deployments to prevent bypass.

Analogy

Think of 429 like a restaurant with limited seating:

  • Rate Limit β†’ “We can only serve 100 customers per hour”
  • 429 Response β†’ “We’re at capacity. Come back in 30 minutes”
  • Retry-After β†’ “We’ll have a table available at 7:00 PM”

Without rate limiting, the restaurant (server) would be overwhelmed and couldn’t serve anyone properly.

Best Practices

  1. Always Include Retry-After - Tell clients when they can retry
  2. Use X-RateLimit- Headers* - Provide limit, remaining, and reset info
  3. Different Limits per Endpoint - More restrictive for expensive operations
  4. Track by User/IP/Key - Not globally (per-resource rate limiting)
  5. Implement Exponential Backoff - Clients should back off if retries fail
  6. Log Rate Limit Hits - Monitor for abuse patterns
  7. Document Rate Limits - Clearly document limits in API documentation
  8. Use 429, not 503 - Distinguish rate limiting from server errors

Common Mistakes

  • No Retry-After Header - Not telling clients when to retry
  • No Rate Limit Info - Not including X-RateLimit-* headers
  • Global Rate Limiting - Not tracking per user/IP (allows unfair usage)
  • Same Limit Everywhere - Not adjusting limits based on endpoint cost
  • Using 503 Instead - Confusing rate limiting with server unavailability
  • Not Logging Violations - Missing abuse patterns and attacks
  • No Client Retry Logic - Clients not respecting Retry-After header
  • Inconsistent Windows - Mixing fixed and sliding window strategies

Standards & RFCs