Skip to main content
Every API key is constrained by two independent limits. Hitting either one returns a 429 Too Many Requests.

The two budgets

FreePartner
Per-minute60 requests600 requests
Monthly quota100,000 requests5,000,000 requests
Window resetSliding 60-second windowFirst of the month, UTC
The per-minute limit catches bursts. The monthly quota keeps things fair across the developer pool. Both are enforced at the Cloudflare edge, so a 429 comes back fast — within a few milliseconds — and doesn’t burn a database round-trip.

Headers

Every authenticated response carries the monthly-quota state:
X-RateLimit-Limit:     100000
X-RateLimit-Remaining: 99873
X-RateLimit-Reset:     1714521600
  • X-RateLimit-Limit — your tier’s monthly quota.
  • X-RateLimit-Remaining — quota left in the current month.
  • X-RateLimit-Reset — Unix timestamp (seconds) when the monthly counter rolls.
The per-minute limit doesn’t get its own header today; if you hit it, the 429 body and Retry-After header tell you the score.

What a 429 looks like

When you exceed either limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 32
Content-Type: application/json
{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded: 60 requests per minute on the free tier.",
    "request_id": "req_01H...",
    "docs_url": "https://docs.elestrals.com/errors/rate_limit_exceeded"
  }
}
Two distinct error codes tell you which limit you hit:
  • rate_limit_exceeded — per-minute burst. Retry-After is in seconds; the budget refills on a sliding window.
  • quota_exceeded — monthly quota. Retry-After is the seconds until the 1st-of-month UTC rollover. You need to wait, upgrade to Partner, or request additional capacity.

Backing off

A polite client respects Retry-After and exponentially backs off if it keeps hitting the wall. A naive retry loop is the fastest way to get a key flagged for abuse.
async function fetchWithBackoff(url, key, attempt = 0) {
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
  if (res.status !== 429) return res;

  const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
  const delayMs = Math.min(retryAfter * 1000, 30_000) + Math.random() * 250;
  await new Promise((r) => setTimeout(r, delayMs));
  if (attempt > 4) throw new Error('rate-limit retries exhausted');
  return fetchWithBackoff(url, key, attempt + 1);
}

Stay under the limit

The single biggest lever is client-side caching. Most TCG data — sets, series, creature definitions — changes infrequently; caching responses for even a few minutes drops your request volume by 10× or more. For predictable workloads (e.g., a nightly sync), schedule the work and avoid bursting. For interactive workloads (e.g., a deck builder), cache by card ID and only fetch on demand.