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:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests allowed per window | 1000 |
X-RateLimit-Remaining | Requests remaining in current window | 995 |
X-RateLimit-Reset | Unix timestamp when the window resets | 1702339200 |
Rate Limits by Tier
Canonical source: canonical-variables.yml (rate_* tokens)
| Tier | Limits | Notes |
|---|---|---|
| Free | No credit card required | |
| Starter | $199/mo | |
| Growth | Higher volume | |
| Scale | Enterprise |
Example Responses
Successful Request (200 OK)
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)
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
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
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
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
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)
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)
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:
- Free → Starter: ($199/mo)
- Starter → Growth:
- 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-Remainingshould 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-IDif issues persist
Related Documentation
- Error Handling - Understanding 429 error responses
- Authentication - Getting your API key
- Pricing - Rate limit tiers
- API Reference - Full API documentation
Support
Questions about rate limits?
- Email: support@firmhound.com
- Docs: https://docs.firmhound.com
- Status: https://status.firmhound.com