Skip to content

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

UnauthenticatedFree API KeyIndie ($9.99/mo)Commercial ($29.99/mo)
Requests/second1 per 3 seconds1 per 2 seconds1 per second1 per second
Daily cap5002,000UnlimitedUnlimited
Identified byIP addressAPI key hashAPI key hashAPI 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:

HeaderDescription
X-RateLimit-LimitMaximum requests in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetSeconds until the current window resets
Retry-AfterSeconds to wait before retrying (only present on 429 responses)

Example response headers:

X-RateLimit-Limit: 2000
X-RateLimit-Remaining: 1847
X-RateLimit-Reset: 86213

When 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

  1. 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.
  2. 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.
  3. 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.
  4. Upgrade for production — unauthenticated and free-tier limits are intentionally restrictive. Any production application serving multiple users should be on a paid tier.