Rate limits and usage
There are two separate limits and they behave nothing alike. A per-minute rate limit protects the service and clears within a minute. A monthly timer allowance is what your plan actually buys and clears when the billing cycle resets. Both return HTTP 429, which is why reading the code rather than the status matters more here than anywhere else in the API.
The two limits
| Rate limit | Timer allowance | |
|---|---|---|
| Counts | Requests per minute | New timers per month |
| Error code | rate_limited | quota_exhausted |
| HTTP status | 429 | 429 |
| Clears | Within a minute | When the billing cycle resets |
| Retry helps | Yes | No |
| Production | 300 per minute | 1,000 – 10,000+ depending on plan |
| Sandbox | 30 per minute | 100 |
The per-minute rate limit
Counted per key and per account in a fixed one-minute window. Three headers on every authenticated response tell you where you stand, so you never have to guess.
On every response
HTTP/1.1 200 OK
RateLimit-Limit: 300
RateLimit-Remaining: 287
RateLimit-Reset: 1893506460429 when exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 18
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Retry after 18 seconds."
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}RateLimit-LimitRequests allowed in the current window.
RateLimit-RemainingRequests left before the limit applies.
RateLimit-ResetUnix seconds at which the window resets.
A fixed window means the counter resets on a boundary rather than sliding. In practice that means bursting immediately after a reset is fine, and bursting across a boundary can use two windows' worth of budget in quick succession without tripping anything.
Handling a 429
Retry-After is authoritative. Honour it rather than computing your own backoff — a client that guesses tends to guess low and get limited again.
Retry on rate_limited, fail fast on quota_exhausted
async function callWithRateLimitHandling(path, options, attempt = 0) {
const response = await fetch(BASE + path, options);
if (response.status === 429) {
const payload = await response.json();
// The two 429s mean very different things.
if (payload.error.code === "quota_exhausted") {
throw new QuotaError(payload.error.details); // waiting will not help
}
if (attempt >= 3) throw new Error("Rate limited after 3 retries");
// Retry-After is authoritative. Do not guess.
const wait = Number(response.headers.get("Retry-After") ?? 1);
await sleep(wait * 1000);
return callWithRateLimitHandling(path, options, attempt + 1);
}
return response;
}The early check for quota_exhausted is the important line. Without it, a retry loop that treats every 429 the same will keep retrying an allowance that cannot refill until the month turns over.
Pacing before you are limited
Better than reacting to a 429 is not causing one. Because every response carries the remaining count, a client can slow itself down as it approaches the boundary.
Read the headers, pace accordingly
// Slow down before you are told to, not after.
let remaining = Infinity;
let resetAt = 0;
async function paced(path, options) {
if (remaining < 10) {
const wait = Math.max(0, resetAt * 1000 - Date.now());
await sleep(wait);
}
const response = await fetch(BASE + path, options);
remaining = Number(response.headers.get("RateLimit-Remaining") ?? Infinity);
resetAt = Number(response.headers.get("RateLimit-Reset") ?? 0);
return response;
}Where limits are usually hit
| Pattern | Better approach |
|---|---|
Polling every timer on an interval | Attach webhook rules. Polling scales with timer count; webhooks do not. |
Paging a large list at limit=25 | Use limit=100 — four times fewer requests for the same data. |
Creating timers in a tight loop | Add a small delay, or batch the work across windows. Creation is also the metered operation. |
One status request per page view | Cache for a few seconds, or render server-side once and tick locally in the browser. |
A load test against Sandbox | Expected — Sandbox allows 30 per minute. Test the Production path against Production limits. |
Reading your usage
GET /usage reports the current billing cycle and every metered metric. It is cheap, creates nothing, and is the right way to check headroom before a bulk job rather than discovering the ceiling halfway through one.
Usage response
GET /usage
{
"data": {
"environment": "production",
"cycle": {
"id": "3f9d6a2c-...",
"starts_at": "2030-01-01T00:00:00.000Z",
"resets_at": "2030-02-01T00:00:00.000Z"
},
"metrics": [
{
"metric": "new_timers",
"total": 214,
"limit": 1000,
"remaining": 786,
"warning_threshold": 0,
"starts_at": "2030-01-01T00:00:00.000Z",
"resets_at": "2030-02-01T00:00:00.000Z"
},
{
"metric": "api_requests",
"total": 48213,
"limit": "unlimited",
"remaining": "unlimited",
"warning_threshold": 0,
"starts_at": "2030-01-01T00:00:00.000Z",
"resets_at": "2030-02-01T00:00:00.000Z"
}
]
},
"request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}| Field | Meaning |
|---|---|
environment | Which environment this key belongs to. A quick way to confirm you are holding what you think. |
cycle.resets_at | When the current billing period ends and allowances refill. |
metrics[].metric | new_timers is the one that can run out. api_requests is reported but unlimited. |
metrics[].total | Consumed so far this cycle. |
metrics[].limit | The ceiling, or "unlimited". |
metrics[].remaining | What is left. Alert on this well before it reaches zero. |
GET /usage/requests is the companion: your API request log, filterable by status, route, key, and request ID. It answers “what did we actually send” without needing your own logs, which is particularly useful for tracking down what consumed an allowance.
When the allowance runs out
Creating a new timer returns quota_exhausted with a details object naming the metric, the limit, the amount used, and the reset time. POST /timers/{timer_id}/duplicate fails the same way, since a duplicate is a new timer.
Everything else keeps working:
- Existing timers keep counting down
- Hosted pages, embeds, and email GIFs keep rendering
- Webhooks keep firing and retrying
- Reads, updates, and control actions all keep working
remaining from GET /usage, not on the first 429.Raising the ceiling means a larger plan, or a Custom arrangement where timer volume, rate limit, key and destination counts, and retention windows are set individually. See API pricing.