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 limitTimer allowance
CountsRequests per minuteNew timers per month
Error coderate_limitedquota_exhausted
HTTP status429429
ClearsWithin a minuteWhen the billing cycle resets
Retry helpsYesNo
Production300 per minute1,000 – 10,000+ depending on plan
Sandbox30 per minute100
There is no monthly cap on API requests on any plan. Requests are not the billing unit — reading a timer's status a million times a month costs nothing beyond staying under the per-minute rate. Only creating timers is metered.

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: 1893506460

429 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-Limit

Requests allowed in the current window.

RateLimit-Remaining

Requests left before the limit applies.

RateLimit-Reset

Unix 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

PatternBetter approach
Polling every timer on an intervalAttach webhook rules. Polling scales with timer count; webhooks do not.
Paging a large list at limit=25Use limit=100 — four times fewer requests for the same data.
Creating timers in a tight loopAdd a small delay, or batch the work across windows. Creation is also the metered operation.
One status request per page viewCache for a few seconds, or render server-side once and tick locally in the browser.
A load test against SandboxExpected — Sandbox allows 30 per minute. Test the Production path against Production limits.
The Sandbox rate limit being ten times lower is deliberate. It surfaces missing retry handling during development, when the consequence is a failed test rather than a failed campaign.

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"
}
FieldMeaning
environmentWhich environment this key belongs to. A quick way to confirm you are holding what you think.
cycle.resets_atWhen the current billing period ends and allowances refill.
metrics[].metricnew_timers is the one that can run out. api_requests is reported but unlimited.
metrics[].totalConsumed so far this cycle.
metrics[].limitThe ceiling, or "unlimited".
metrics[].remainingWhat 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
So a quota problem degrades new work rather than breaking live campaigns — which is the right failure mode, but also means nobody notices unless you are watching. Alert on 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.