Rate Limits & Quotas

Understand API rate limits and plan your integration

Two Rate Limits

Requests are counted twice, against two independent limits. Both return 429 RATE_LIMITED.

LimitCounted perApplies to
100 requests / secondAPI keyevery authenticated endpoint
500 requests / minuteclient IPevery endpoint, authenticated or not

The per-key limit is the one an integration meets first: a loop that sends without pacing hits 100/s long before it hits 500/min. Its error message names the limit — Rate limit exceeded: 100 requests/second per API key.

The per-minute limit is per client IP, not per tenant. Two workspaces calling from the same egress IP share it; one workspace calling from several IPs gets one allowance per IP.

POST /v1/templates/render-preview has its own per-IP limit of 60 requests / minute, which takes the place of the 500/minute on that endpoint. The per-key limit still applies to it.

Rate Limit Headers

Responses carry the state of the per-IP limit:

x-ratelimit-limit: 500
x-ratelimit-remaining: 245
x-ratelimit-reset: 47

x-ratelimit-reset is the number of seconds remaining in the current window, not a Unix timestamp. When the per-IP limit is exceeded, the 429 also carries a retry-after header, in seconds.

These headers describe the per-IP limit only: 500/minute, or 60/minute on render-preview. The per-key limit of 100/second is not reflected in them: you can be at x-ratelimit-remaining: 400 and still get a 429 for sending 100 requests in the same second. That per-key 429 carries neither these headers nor retry-after.

Check Rate Limit Status

Before hitting the limit, check the headers:

curl -i https://api.tratto.email/v1/emails \
  -H "Authorization: Bearer tratto_live_..."

Returns headers with remaining requests and reset time.

Rate Limit Exceeded (429)

Both limits answer with the same code. Over the per-IP limit:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded, retry in 47 seconds",
    "docs": "https://docs.tratto.email/en/docs/error-codes"
  }
}

Over the per-key limit, message is Rate limit exceeded: 100 requests/second per API key.

The body carries no retryAfter field. After a per-IP 429, wait the retry-after seconds. After a per-key 429 there is no header to read, but the window is one second, so a short backoff is enough.

Retry Strategy

Use exponential backoff with jitter, honour retry-after when it is set, and do not retry QUOTA_EXCEEDED, which shares the 429 status:

async function sendWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, options);
    if (res.status !== 429 || attempt === maxRetries) return res;

    const { error } = await res.clone().json();
    if (error?.code === 'QUOTA_EXCEEDED') return res; // retrying will not help

    const retryAfter = Number(res.headers.get('retry-after'));
    const delay = retryAfter
      ? retryAfter * 1000
      : 2 ** attempt * 1000 + Math.random() * 1000;
    await new Promise((r) => setTimeout(r, delay));
  }
}

Plan Limits

Each plan has two caps:

PlanEmails per monthDomains
Free3,0002
Starter75,0005
Growth500,000unlimited

There is no per-day cap on a plan. The only daily limit in the product is the test-mode one — 100 test sends per day, see Test Mode.

Exceed the monthly quota and requests fail with QUOTA_EXCEEDED, which also returns 429. Unlike RATE_LIMITED it will not clear by retrying: see Error Codes.

The domain limit counts every domain in the workspace, whatever its verification status, and it is checked only when you add one (POST /v1/domains). Deleting a domain (DELETE /v1/domains/{id}) frees its place.

Read your own limits from the API

Do not hard-code the numbers above. GET /v1/workspace returns the limits that are actually enforced on your workspace, and null means no limit:

curl https://api.tratto.email/v1/workspace \
  -H "Authorization: Bearer tratto_live_..."
{
  "data": {
    "plan": "free",
    "limits": { "emailsPerMonth": 3000, "domains": 2 }
  }
}

This is the same table the enforcement reads, so it cannot disagree with what you are actually allowed to send.

The month is a UTC calendar month

The quota is not a rolling 30-day window. The counter is keyed on the UTC calendar month (2026-09), and it rolls over on your first send of the new month — there is no scheduled reset job. A workspace that sends nothing in October still starts November at zero, on its first November send.

There is no "Pro" plan, and no plan without a monthly email cap. The plans are the ones in the table above, and GET /v1/workspace always reports one of them.

Burst Behavior

Neither limit is averaged, and neither allows a burst above it. The per-key limit counts requests in each clock second. The per-IP limit opens a one-minute window on your first request and resets when that window ends. Spread requests out instead of sending them in spikes.


Next: Error Codes


Edit this page on GitHub

Last updated on