API Rate Limiting

Rate limit detection, client-side throttling, and API-specific limits.

Rate Limit Headers

Common rate limit response headers
# Inspect rate limit status
curl -sI -H "Authorization: Bearer $TOKEN" \
  https://api.example.com/v1/devices | grep -i 'x-ratelimit\|retry-after'

# Typical headers:
# X-RateLimit-Limit: 1000          total allowed per window
# X-RateLimit-Remaining: 847       calls left in this window
# X-RateLimit-Reset: 1712345678    epoch when window resets
# Retry-After: 30                  seconds to wait (on 429)
Parse reset time — know when you can resume
reset=$(curl -sI https://api.example.com/v1/devices | \
  grep -i x-ratelimit-reset | awk '{print $2}' | tr -d '\r')
echo "Rate limit resets at: $(date -d @"$reset" '+%H:%M:%S')"
echo "Wait $(( reset - $(date +%s) )) seconds"

Client-Side Rate Limiting

Bash — throttle requests with sleep
# 5 requests per second = 0.2s between requests
while IFS= read -r device_id; do
    curl -s "https://api.example.com/v1/devices/$device_id" | jq .name
    sleep 0.2
done < device_ids.txt
Bash — respect 429 with retry
api_call() {
    local url="$1"
    local max_retries=3
    for attempt in $(seq 1 $max_retries); do
        response=$(curl -s -w '\n%{http_code}' "$url")
        status=$(echo "$response" | tail -1)
        body=$(echo "$response" | sed '$d')

        if [[ "$status" -eq 200 ]]; then
            echo "$body"
            return 0
        elif [[ "$status" -eq 429 ]]; then
            retry_after=$(curl -sI "$url" | grep -i retry-after | awk '{print $2}' | tr -d '\r')
            echo "Rate limited, waiting ${retry_after:-5}s (attempt $attempt)" >&2
            sleep "${retry_after:-5}"
        else
            echo "HTTP $status" >&2
            return 1
        fi
    done
    echo "Exhausted retries" >&2
    return 1
}
Python httpx — token bucket pattern
import httpx
import time

class RateLimiter:
    def __init__(self, calls_per_second: float = 5.0):
        self.interval = 1.0 / calls_per_second
        self.last_call = 0.0

    def wait(self):
        elapsed = time.monotonic() - self.last_call
        if elapsed < self.interval:
            time.sleep(self.interval - elapsed)
        self.last_call = time.monotonic()

limiter = RateLimiter(calls_per_second=5)

with httpx.Client() as client:
    for device_id in device_ids:
        limiter.wait()
        response = client.get(f"https://api.example.com/v1/devices/{device_id}")
        print(response.json()["name"])

API-Specific Limits

ISE ERS — default 100 concurrent sessions, no explicit rate limit header
# ISE ERS has no rate limit headers but will return 500 under heavy load
# Safe pattern: sequential with small delay
for mac in "${mac_list[@]}"; do
    curl -sk -u "$ISE_USER:$ISE_PASS" \
      -H "Accept: application/json" \
      "https://10.50.1.20:9060/ers/config/endpoint?filter=mac.EQ.$mac" | \
      jq -r '.SearchResult.resources[0].name // "not found"'
    sleep 0.5
done
Meraki — 10 calls/second per org (dashboard API v1)
# Meraki returns 429 with Retry-After header
# Always check: X-RateLimit-Remaining
curl -sI -H "X-Cisco-Meraki-API-Key: $MERAKI_KEY" \
  "https://api.meraki.com/api/v1/organizations" | grep -i ratelimit
GitHub API — 5000 requests/hour for authenticated users
# Check current rate limit status
gh api rate_limit --jq '.rate | "Remaining: \(.remaining)/\(.limit), resets: \(.reset | todate)"'

FastAPI Rate Limiting

slowapi middleware for domus-api
from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@app.get("/v1/devices")
@limiter.limit("100/minute")
async def list_devices(request: Request):
    return await fetch_devices()