Thinkific GraphQL Rate Limits: A Field Guide to Cost Budgets

Thinkific's GraphQL API has two ceilings that aren't documented prominently: 1000 cost units per query and 2000 cost units per minute. Hit either and you get RATE_LIMITED or MAX_QUERY_COST_EXCEEDED. Here's how the numbers actually behave and what to do about them.

The two ceilings

First ceiling: a single GraphQL query cannot cost more than 1000 units. Exceed it and the response is an error, not a partial result: `"Query cost of 1253 exceeds the maximum single query cost limit of 1000."` Second ceiling: a rolling per-minute budget of roughly 2000 units. Burn through it and subsequent queries fail with `extensions.code: RATE_LIMITED` and message `API rate limit exceeded.` The single-query limit is enforced before execution. The per-minute limit is enforced as you go.

How cost is calculated (in practice)

Cost scales with the `first` argument on every Connection you traverse. A query that pulls `chapters(first: 20) { lessons(first: 50) }` is roughly proportional to 20 × 50 = 1000 — which is exactly the ceiling, so any quiz fields or PDF URLs you add to the selection set push you over. The fix isn't restructuring the query; it's lowering `first` per level. For our Theory Time exporter we landed on `chapterFirst: 30, lessonFirst: 40` after iterating — which keeps a single chapter+lessons traversal under ~1000 for the widest course we had.

The --all cascade failure

If you process courses sequentially in a single export run, the first course consumes most of your minute budget and every course after it errors out with RATE_LIMITED. This is the most painful failure mode because it looks like a code bug — only one course in your export folder, sixteen empty `errors.json` files. The mitigation that actually works is pacing between courses: wait ~30 seconds between course-structure queries, or split structure-export from data-export into separate runs entirely (which is what we ended up doing in production).

Required pagination args

Every GraphQL Connection in Thinkific's schema requires either `first` or `last`. Omit them and you get `"You must pass either first or last to paginate the 'lessons' connection."` There are no defaults. This trips up nearly every example query you'll find online because most are written assuming connection defaults of 10 or 25 — Thinkific has none. Add `first` everywhere or the query won't run.

Backoff vs. pacing

REST and GraphQL fail in different ways and want different mitigations. REST 429s respond to exponential backoff (1s, 2s, 4s, 8s) — retries clear once the per-endpoint rolling window resets. GraphQL cost-budget rate limits do not — there is no per-endpoint window, just the shared minute budget. Retrying immediately just spends more of a budget you already exhausted. The correct response is to sleep for the remainder of the minute and then continue. Most rate-limit libraries are designed for HTTP 429 with Retry-After; GraphQL extensions errors need a separate handler.

Query introspection helps

Thinkific's GraphQL endpoint supports standard introspection (`__schema`, `__type`). Before writing a real query, introspect the relevant types to see exactly which fields exist on `VideoContent`, `PdfContent`, `QuizContent`, and `Lesson`. The cost of the introspection query itself is small. We spent more hours guessing at field names from the documentation than we spent introspecting the real schema once — the field names in the docs don't always match what's exposed live.

Last updated: