Rate Limits
Rate limits protect the AniKura API from abuse and ensure fair access for all consumers. Limits are enforced per API key (or per IP address for unauthenticated requests) using sliding-window counters in DragonflyDB.
Limits by Tier
| Unauthenticated | Free API Key | Indie ($9.99/mo) | Commercial ($29.99/mo) | |
|---|---|---|---|---|
| Requests/second | 1 per 3 seconds | 1 per 2 seconds | 1 per second | 1 per second |
| Daily cap | 500 | 2,000 | Unlimited | Unlimited |
| Identified by | IP address | API key hash | API key hash | API key hash |
Unauthenticated limits are intentionally restrictive to encourage free key registration. A free API key takes about 30 seconds to create and gives 4x the daily request quota.
Rate Limit Headers
Every response includes these headers — even when the request is not rate-limited:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Seconds until the current window resets |
Retry-After | Seconds to wait before retrying (only present on 429 responses) |
Example response headers:
X-RateLimit-Limit: 2000X-RateLimit-Remaining: 1847X-RateLimit-Reset: 86213When Rate Limited
When you exceed the rate limit, the API returns HTTP 429 Too Many Requests:
{ "errors": [ { "message": "Rate limit exceeded. Please wait before retrying.", "extensions": { "code": "RATE_LIMITED" } } ]}When the daily cap is exceeded, the message is:
Daily request limit exceeded.The daily cap resets at midnight UTC.
Handling 429 Responses
Exponential backoff
Do not immediately retry after a 429. Use exponential backoff with jitter:
async function fetchWithBackoff( url: string, options: RequestInit, maxRetries = 5): Promise<Response> { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options);
if (response.status !== 429) { return response; }
const retryAfter = parseInt(response.headers.get("Retry-After") ?? "1", 10); // Add jitter: wait (retryAfter + random 0-1s) * 2^attempt const delay = (retryAfter + Math.random()) * Math.pow(2, attempt) * 1000; console.warn(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, delay)); }
throw new Error("Max retries exceeded after rate limit");}Read Retry-After header
The Retry-After header tells you exactly how many seconds to wait before the next request will
succeed. Always honor this value rather than using arbitrary delays.
Proactive quota management
Check X-RateLimit-Remaining in every response. If it drops below a threshold, slow down your
request rate proactively:
function shouldSlowDown(response: Response): boolean { const remaining = parseInt(response.headers.get("X-RateLimit-Remaining") ?? "999", 10); // Start backing off when fewer than 100 requests remain return remaining < 100;}Best Practices
- Cache responses — anime/manga metadata changes infrequently. A 5-minute cache for search results and a 1-hour cache for detail pages saves significant quota.
- Batch queries — GraphQL allows fetching multiple fields in a single request. Fetch all the data you need for a page in one query, not multiple requests.
- Register a free key — even if you don’t need the full quota, having a key makes it easier to diagnose rate limit issues and upgrade later.
- Upgrade for production — unauthenticated and free-tier limits are intentionally restrictive. Any production application serving multiple users should be on a paid tier.