Rate Limits Specification
Overview: GigPilot AI enforces rate limits on API traffic using
@fastify/rate-limitwith an Upstash Redis backing store. Rate limits protect system availability, mitigate denial-of-service attempts, and ensure fair resource allocation across user subscription tiers.
Tier-Based Limits Matrix
Rate limits are evaluated based on user account tier and authenticated API token. Unauthenticated traffic defaults to IP-based rate limiting.
| Account Tier / Context | Window Size | Max Requests | Storage Engine | Key Strategy |
|---|---|---|---|---|
| Public / Unauthenticated | 1 Minute | 60 requests | Redis | Client IP (req.ip) |
| Free Plan Users | 1 Minute | 100 requests | Redis | User ID (req.user.id) |
| Pro Plan Users | 1 Minute | 300 requests | Redis | User ID (req.user.id) |
| Agency Plan Users | 1 Minute | 1,000 requests | Redis | User ID (req.user.id) |
| Background Scheduler | Unlimited | No limit | N/A | X-Cron-Secret bypass |
HTTP Rate Limit Headers
Every HTTP response includes standard rate limit headers indicating window allowance and remaining requests:
RateLimit-Limit: 100RateLimit-Remaining: 95RateLimit-Reset: 1722470460Retry-After: 45| Header | Description |
|---|---|
RateLimit-Limit |
Maximum allowed requests in the current 60-second window |
RateLimit-Remaining |
Remaining requests allowed within the current window |
RateLimit-Reset |
Unix timestamp (seconds) when the current window resets |
Retry-After |
Seconds to wait before making another request (sent on 429 errors) |
Rate Limit Failure Response (HTTP 429)
When a client exceeds their allocated quota within the time window, the server returns an HTTP 429 Too Many Requests error:
{ "statusCode": 429, "error": "Too Many Requests", "message": "Rate limit exceeded. You have sent too many requests in a short period. Please try again in 45 seconds.", "retryAfterSeconds": 45}Client-Side Retry Implementation
Clients should implement exponential backoff with jitter when encountering 429 status codes:
async function fetchWithExponentialBackoff( url: string, options: RequestInit, maxRetries = 4): Promise<Response> { let attempt = 0;
while (attempt < maxRetries) { const response = await fetch(url, options);
if (response.status !== 429) { return response; }
attempt++; const retryAfterHeader = response.headers.get('Retry-After'); const delaySeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : Math.pow(2, attempt) + Math.random();
console.warn(`[429 Rate Limited] Retrying attempt ${attempt}/${maxRetries} after ${delaySeconds}s`); await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000)); }
throw new Error(`Request failed after ${maxRetries} rate limit retries.`);}
