Rate limits and quotas
Two different limits, measured differently. A rate limit is about the next second; a quota is about the month. Both are visible before you hit them.
The headers, on every response
These come back on successful responses too, not only on a 429. A client that only learns its limit by being refused cannot avoid being refused.
RateLimit-Policy: "plan";q=20;w=2, "product";q=20;w=2, "key";q=20;w=2
RateLimit: "key";r=19;t=1
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 19
X-RateLimit-Reset: 1RateLimit-Policylists every bucket this request passed through.qis how many requests the bucket holds when full, andwis how many seconds it takes to refill from empty.q=20;w=2means 20 at once, refilled at 10 a second.RateLimitnames the bucket closest to empty.ris what is left in it andtis the seconds until it is full again.- The bucket names depend on the credential:
keyfor a project key,agentfor a connected AI tool,dashboardfor a signed-in person. The numbers come from your plan.
On a 429 there is also Retry-After, in seconds. Honour it — it is the real number, and guessing is how a thundering herd gets built.
Per second
A token bucket, so a short burst above your steady rate is fine as long as the average holds. "30 requests a second, burst 60" means exactly that: you may spend 60 at once, and it refills at 30 a second.
r then stays the same from one response to the next. That is the limiter working, not missing. To see a 429, send a burst in parallel, larger than q.A parallel burst of 60 — expect some 429s with Retry-After
CMSKITE_KEY=csk_live_... node -e '
const h = { authorization: "Bearer " + process.env.CMSKITE_KEY }
const url = "https://api.cmskite.com/v1/blog/posts?limit=1"
Promise.all(Array.from({ length: 60 }, () => fetch(url, { headers: h }))).then((rs) => {
const c = {}
for (const r of rs) c[r.status] = (c[r.status] ?? 0) + 1
const hit = rs.find((r) => r.status === 429)
console.log(c, hit ? { retryAfter: hit.headers.get("retry-after"), policy: hit.headers.get("ratelimit") } : "no 429")
})'With q=20;w=2 expect roughly 20 to 40 answers of 200 (the bucket, plus what refills while the burst is in flight) and the rest 429.
Your steady rate and burst come from your plan, and the live figures are on the plans page. GET /v1/billing returns the exact numbers your key is held to.
Several buckets apply at once — your plan, the product, the individual key, the route — and all of them have to pass. Which one refused you is in details.policy on the 429.
Per month
Your plan includes a number of API requests per billing period. Reads served from your own cache do not count, which is most of the reason a sixty-second revalidate is worth more than an upgrade.
Free, Starter and Team include a fixed number of requests a month; on Custom you choose the tier. Past the limit a request is refused with 429 QUOTA_EXCEEDED — it is never silently billed.
When the platform is at capacity
Above a concurrency ceiling an instance answers 503 SERVICE_UNAVAILABLE with a short Retry-After instead of queueing. That is deliberate. A refused request costs nothing and is survivable; a queued one is how an instance takes itself down, and everybody waiting behind it with it.
Treat a 503 as "try again in a moment", not as an outage. It means the platform chose to stay up.
Staying under
- Cache. A published post changes rarely and is read constantly.
- Ask for what you need:
?limit=100once beats twenty-five requests of four. - Use
includeand the relations already on a listing rather than a request per row. A listing returns its categories, tags and authors with it. - Read
RateLimit-Remainingand slow down before it reaches zero. - Back off exponentially with jitter when you are refused, and honour
Retry-Afterwhen it is present.







