Skip to content

REST API - Rate Limiting

Complete guide to understanding and managing API rate limits to ensure optimal performance and fair usage.

📋 Table of Contents


Overview

Rate limiting protects the API from abuse and ensures fair usage for all clients. The HelpDesk Pro API implements rate limiting based on:

  • Endpoint Type - Different limits for different endpoints
  • User Authentication - Authenticated users have higher limits
  • IP Address - Limits apply per IP for unauthenticated requests

Why Rate Limiting?

  • Prevents Abuse - Protects against DDoS and spam
  • Ensures Fair Usage - Distributes resources fairly
  • Maintains Performance - Keeps API responsive
  • Cost Control - Manages server resources

Rate Limit Tiers

General API Endpoints

Limit: 60 requests per minute

Applies to:

  • All authenticated endpoints
  • Per user (if authenticated) or per IP (if unauthenticated)

Examples:

  • GET /api/v1/tickets
  • POST /api/v1/tickets
  • GET /api/v1/contacts
  • GET /api/v1/dashboard

Authentication Endpoints

Limit: 5 requests per minute

Applies to:

  • Per IP address (to prevent brute force attacks)

Examples:

  • POST /api/v1/auth/login
  • POST /api/v1/auth/register
  • POST /api/v1/auth/password/reset

Rate Limit Window

  • Window: 1 minute (60 seconds)
  • Reset: Automatically resets every minute
  • Tracking: Per user ID or IP address

Rate Limit Headers

Every API response includes rate limit information in headers:

Response Headers

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
Retry-After: 15
HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed per window60
X-RateLimit-RemainingRequests remaining in current window45
Retry-AfterSeconds to wait before retrying (only on 429)15

Example Response

http
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45

{
  "success": true,
  "data": [...]
}

Rate Limit Exceeded Response

http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 15

{
  "success": false,
  "message": "Too Many Requests",
  "meta": {
    "timestamp": "2025-01-15T10:30:00.000000Z",
    "version": "v1"
  }
}

Handling Rate Limits

Detecting Rate Limits

Check for 429 status code in responses:

javascript
const response = await fetch(url, options);

if (response.status === 429) {
  // Rate limit exceeded
  const retryAfter = response.headers.get('Retry-After');
  console.log(`Rate limited. Retry after ${retryAfter} seconds`);
}

Implementing Retry Logic

Basic Retry:

javascript
async function requestWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
      console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      continue;
    }
    
    return response;
  }
  
  throw new Error('Max retries exceeded');
}

Exponential Backoff:

javascript
async function requestWithExponentialBackoff(url, options, maxRetries = 5) {
  for (let i = 0; i < maxRetries; i++) {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
      const waitTime = Math.min(retryAfter * 1000, Math.pow(2, i) * 1000);
      
      console.log(`Rate limited. Waiting ${waitTime}ms...`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      continue;
    }
    
    return response;
  }
  
  throw new Error('Max retries exceeded');
}

Monitoring Rate Limit Headers

Track your usage to avoid hitting limits:

javascript
class RateLimitTracker {
  constructor() {
    this.limit = 60;
    this.remaining = 60;
  }

  updateFromHeaders(headers) {
    this.limit = parseInt(headers.get('X-RateLimit-Limit') || '60');
    this.remaining = parseInt(headers.get('X-RateLimit-Remaining') || '0');
  }

  getUsagePercentage() {
    return ((this.limit - this.remaining) / this.limit) * 100;
  }

  isNearLimit(threshold = 80) {
    return this.getUsagePercentage() >= threshold;
  }

  getRemainingRequests() {
    return this.remaining;
  }
}

// Usage
const tracker = new RateLimitTracker();

const response = await fetch(url, options);
tracker.updateFromHeaders(response.headers);

if (tracker.isNearLimit()) {
  console.warn('Approaching rate limit. Consider throttling requests.');
}

console.log(`Remaining requests: ${tracker.getRemainingRequests()}`);

Best Practices

1. Implement Request Throttling

Limit your request rate to stay under limits:

javascript
class RequestThrottler {
  constructor(maxRequests, windowMs) {
    this.maxRequests = maxRequests;
    this.windowMs = windowMs;
    this.requests = [];
  }

  async wait() {
    const now = Date.now();
    
    // Remove old requests outside the window
    this.requests = this.requests.filter(
      time => now - time < this.windowMs
    );

    // If at limit, wait until oldest request expires
    if (this.requests.length >= this.maxRequests) {
      const oldestRequest = this.requests[0];
      const waitTime = this.windowMs - (now - oldestRequest);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      this.requests.shift();
    }

    this.requests.push(Date.now());
  }
}

// Usage: Limit to 50 requests per minute (under the 60 limit)
const throttler = new RequestThrottler(50, 60000);

async function makeRequest() {
  await throttler.wait();
  return fetch(url, options);
}

2. Cache Responses

Reduce API calls by caching responses:

javascript
class APICache {
  constructor(ttl = 60000) { // 1 minute default
    this.cache = new Map();
    this.ttl = ttl;
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }
    
    return item.data;
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
  }
}

const cache = new APICache(60000); // 1 minute cache

async function getCachedTickets() {
  const cacheKey = 'tickets';
  const cached = cache.get(cacheKey);
  
  if (cached) {
    return cached;
  }
  
  const response = await fetch('/api/v1/tickets');
  const data = await response.json();
  cache.set(cacheKey, data);
  
  return data;
}

3. Batch Requests

Combine multiple requests when possible:

javascript
// Instead of multiple requests:
// GET /api/v1/tickets/1
// GET /api/v1/tickets/2
// GET /api/v1/tickets/3

// Use filtering:
// GET /api/v1/tickets?id=1,2,3

4. Use Webhooks

For real-time updates, use webhooks instead of polling:

javascript
// Instead of polling every second:
setInterval(() => {
  fetch('/api/v1/tickets');
}, 1000);

// Use webhooks for updates
app.post('/webhook/tickets', (req, res) => {
  // Handle ticket updates
});

5. Monitor Usage

Track your API usage to optimize:

javascript
class UsageMonitor {
  constructor() {
    this.requests = [];
    this.windowMs = 60000; // 1 minute
  }

  recordRequest() {
    const now = Date.now();
    this.requests = this.requests.filter(
      time => now - time < this.windowMs
    );
    this.requests.push(now);
  }

  getRequestRate() {
    return this.requests.length;
  }

  getAverageRequestRate() {
    if (this.requests.length === 0) return 0;
    const oldest = this.requests[0];
    const newest = this.requests[this.requests.length - 1];
    const duration = (newest - oldest) / 1000; // seconds
    return duration > 0 ? this.requests.length / duration * 60 : 0;
  }
}

Implementation Examples

JavaScript/TypeScript

typescript
class RateLimitedAPI {
  private baseURL: string;
  private token: string;
  private throttler: RequestThrottler;
  private tracker: RateLimitTracker;

  constructor(baseURL: string, token: string) {
    this.baseURL = baseURL;
    this.token = token;
    this.throttler = new RequestThrottler(50, 60000); // 50 req/min
    this.tracker = new RateLimitTracker();
  }

  async request(endpoint: string, options: RequestInit = {}) {
    // Wait for throttler
    await this.throttler.wait();

    const response = await fetch(`${this.baseURL}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        ...options.headers,
      },
    });

    // Update tracker
    this.tracker.updateFromHeaders(response.headers);

    // Handle rate limit
    if (response.status === 429) {
      const retryAfter = parseInt(
        response.headers.get('Retry-After') || '60'
      );
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      return this.request(endpoint, options); // Retry
    }

    return response;
  }

  getUsageInfo() {
    return {
      limit: this.tracker.limit,
      remaining: this.tracker.remaining,
      usagePercentage: this.tracker.getUsagePercentage(),
    };
  }
}

Python

python
import time
import requests
from collections import deque
from typing import Optional

class RateLimitedAPI:
    def __init__(self, base_url: str, token: str, max_requests: int = 50):
        self.base_url = base_url
        self.token = token
        self.max_requests = max_requests
        self.window_seconds = 60
        self.requests = deque()
        self.limit = 60
        self.remaining = 60

    def _wait_if_needed(self):
        now = time.time()
        
        # Remove old requests
        while self.requests and now - self.requests[0] > self.window_seconds:
            self.requests.popleft()
        
        # Wait if at limit
        if len(self.requests) >= self.max_requests:
            oldest = self.requests[0]
            wait_time = self.window_seconds - (now - oldest)
            if wait_time > 0:
                time.sleep(wait_time)
                self.requests.popleft()
        
        self.requests.append(time.time())

    def request(self, method: str, endpoint: str, **kwargs):
        self._wait_if_needed()
        
        url = f"{self.base_url}{endpoint}"
        headers = {
            'Authorization': f'Bearer {self.token}',
            'Content-Type': 'application/json',
            'Accept': 'application/json',
            **kwargs.get('headers', {}),
        }
        
        response = requests.request(method, url, headers=headers, **kwargs)
        
        # Update rate limit info
        self.limit = int(response.headers.get('X-RateLimit-Limit', 60))
        self.remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
        
        # Handle rate limit
        if response.status_code == 429:
            retry_after = int(response.headers.get('Retry-After', 60))
            time.sleep(retry_after)
            return self.request(method, endpoint, **kwargs)  # Retry
        
        return response

    def get_usage_info(self):
        return {
            'limit': self.limit,
            'remaining': self.remaining,
            'usage_percentage': ((self.limit - self.remaining) / self.limit) * 100,
        }

Monitoring Usage

Track Rate Limit Headers

javascript
function trackRateLimitUsage(response) {
  const limit = parseInt(response.headers.get('X-RateLimit-Limit') || '60');
  const remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || '0');
  const usage = ((limit - remaining) / limit) * 100;
  
  console.log(`API Usage: ${usage.toFixed(1)}% (${remaining}/${limit} remaining)`);
  
  if (usage > 80) {
    console.warn('⚠️ Approaching rate limit!');
  }
  
  // Send to analytics
  analytics.track('api_rate_limit_usage', {
    limit,
    remaining,
    usage,
  });
}

Alert on High Usage

javascript
class RateLimitAlert {
  constructor(threshold = 80) {
    this.threshold = threshold;
    this.alerted = false;
  }

  check(usagePercentage) {
    if (usagePercentage >= this.threshold && !this.alerted) {
      this.alerted = true;
      this.sendAlert(usagePercentage);
    } else if (usagePercentage < this.threshold) {
      this.alerted = false;
    }
  }

  sendAlert(usagePercentage) {
    console.warn(`🚨 Rate limit usage at ${usagePercentage}%!`);
    // Send email, Slack notification, etc.
  }
}

Troubleshooting

Issue: Frequently Hitting Rate Limits

Solutions:

  1. Implement request throttling
  2. Cache responses when possible
  3. Reduce polling frequency
  4. Use webhooks instead of polling
  5. Batch multiple requests

Issue: 429 Errors Even with Throttling

Solutions:

  1. Check if multiple clients are using the same token
  2. Verify throttling is working correctly
  3. Reduce max requests per minute
  4. Implement exponential backoff

Issue: Inconsistent Rate Limit Headers

Solutions:

  1. Ensure you're reading headers correctly
  2. Check for proxy/CDN modifying headers
  3. Verify you're using the latest API version
  4. Contact support if issue persists

Next Steps


For questions about rate limits or to request higher limits, contact our API support team.

Released under the MIT License.