Skip to content

Rate Limit Headers

Task: FS-6009, FS-6016 Status: Implemented Version: 1.1.0 (Updated Dec 13, 2025)

Overview

All Firmhound API responses include rate limit headers to help developers track their usage and avoid hitting limits.

Implementation: PostgreSQL-backed distributed rate limiting (FS-6016)

  • Works across multiple Cloud Run instances
  • Fixed-window algorithm: burst (5s), minute (60s), day (24h)
  • Fails open if database unavailable

Headers

Every API response (except internal/health endpoints) includes these headers:

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed per window1000
X-RateLimit-RemainingRequests remaining in current window995
X-RateLimit-ResetUnix timestamp when the window resets1702339200

Rate Limits by Tier

Canonical source: canonical-variables.yml (rate_* tokens)

TierLimitsNotes
FreeNo credit card required
Starter$199/mo
GrowthHigher volume
ScaleEnterprise

Example Responses

Successful Request (200 OK)

http
GET /v1/gps?state=CA HTTP/1.1
Authorization: Bearer <your_token>

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 1702339200

{
  "data": [
    { "crd": "12345", "name": "Example GP" }
  ],
  "meta": {
    "total": 1,
    "page": 1
  }
}

Rate Limit Exceeded (429)

http
GET /v1/gps HTTP/1.1
Authorization: Bearer <your_token>

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

{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded for tier: free",
    "request_id": "abc123",
    "doc_url": "https://docs.firmhound.com/errors#rate_limit_exceeded"
  }
}

Unlimited Tier

http
GET /v1/gps HTTP/1.1
Authorization: Bearer <premier_token>

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: unlimited
X-RateLimit-Remaining: unlimited
X-RateLimit-Reset: 1702339200

{
  "data": [ ... ]
}

Best Practices

1. Check Headers Before Making Requests

javascript
async function makeRequest(url) {
  const response = await fetch(url, {
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  });

  // Check remaining requests
  const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
  const limit = parseInt(response.headers.get('X-RateLimit-Limit'));

  console.log(`${remaining}/${limit} requests remaining`);

  if (remaining < 10) {
    console.warn('Rate limit warning: Less than 10 requests remaining!');
  }

  return response.json();
}

2. Handle Rate Limit Exceeded

javascript
async function makeRequestWithRetry(url) {
  const response = await fetch(url, {
    headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
  });

  if (response.status === 429) {
    const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
    const now = Math.floor(Date.now() / 1000);
    const waitSeconds = resetTime - now;

    console.log(`Rate limited. Waiting ${waitSeconds} seconds...`);

    // Wait and retry
    await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
    return makeRequestWithRetry(url);
  }

  return response.json();
}

3. Track Usage Over Time

python
import time
import requests

class RateLimitedClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.base_url = "https://api.firmhound.com"
        self.headers = {"Authorization": f"Bearer {api_key}"}

    def get(self, endpoint):
        response = requests.get(
            f"{self.base_url}{endpoint}",
            headers=self.headers
        )

        # Log rate limit info
        limit = response.headers.get('X-RateLimit-Limit')
        remaining = response.headers.get('X-RateLimit-Remaining')
        reset = response.headers.get('X-RateLimit-Reset')

        print(f"Rate Limit: {remaining}/{limit} (resets at {reset})")

        if response.status_code == 429:
            reset_time = int(reset)
            wait_time = reset_time - int(time.time())
            print(f"Rate limited. Waiting {wait_time}s...")
            time.sleep(wait_time)
            return self.get(endpoint)  # Retry

        response.raise_for_status()
        return response.json()

# Usage
client = RateLimitedClient("your_api_key")
data = client.get("/v1/gps?state=CA")

Excluded Endpoints

These endpoints do NOT include rate limit headers:

  • / - Root health check
  • /health - Health check
  • /status - Status check
  • /_internal/* - Internal endpoints

Reset Time Calculation

The X-RateLimit-Reset header contains a Unix timestamp (seconds since epoch).

Convert to Local Time (JavaScript)

javascript
const resetTime = parseInt(response.headers.get('X-RateLimit-Reset'));
const resetDate = new Date(resetTime * 1000);
console.log(`Rate limit resets at: ${resetDate.toLocaleString()}`);

Convert to Local Time (Python)

python
import datetime

reset_time = int(response.headers.get('X-RateLimit-Reset'))
reset_date = datetime.datetime.fromtimestamp(reset_time)
print(f"Rate limit resets at: {reset_date}")

Upgrading Your Plan

If you need higher rate limits:

  1. Free → Starter: ($199/mo)
  2. Starter → Growth:
  3. Growth → Scale:

Contact support@firmhound.com for plan upgrades.

Troubleshooting

Headers Not Present

If you don't see rate limit headers:

  • Check that you're not hitting an excluded endpoint (/, /health, etc.)
  • Verify you're using the correct API base URL
  • Check for proxy/CDN caching (may strip headers)

Reset Time in the Past

If X-RateLimit-Reset is in the past:

  • Window has already reset
  • Your next request will start a new window
  • X-RateLimit-Remaining should be refreshed

Remaining Count Doesn't Decrease

Both development and production use PostgreSQL-backed rate limiting (FS-6016):

  • Remaining should decrease with each request
  • If not, verify database connectivity
  • Contact support with your X-Request-ID if issues persist

Support

Questions about rate limits?

Firmhound API Documentation