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.
| Limit | Counted per | Applies to |
|---|---|---|
| 100 requests / second | API key | every authenticated endpoint |
| 500 requests / minute | client IP | every 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: 47x-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:
| Plan | Emails per month | Domains |
|---|---|---|
| Free | 3,000 | 2 |
| Starter | 75,000 | 5 |
| Growth | 500,000 | unlimited |
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