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