Appearance
Rate Limiting
Krafter Mail enforces rate limits to protect the service from abuse and to keep one team's traffic from affecting another. Limits are evaluated per team using a fixed window.
Limits
| Resource | Limit | Window |
|---|---|---|
| All API endpoints | 100 requests | 60 seconds (fixed) |
The window starts at floor(now / 60) * 60, so all callers in the same calendar minute share one bucket. The bucket is keyed on the authenticated team — two API keys belonging to the same team share the same counter.
INFO
This is a fixed window, not a sliding window. When the window rolls over, the counter resets to zero.
Rate Limit Headers
Every API response — both 200 OK and 429 Too Many Requests — includes the current bucket's headers:
http
HTTP/1.1 200 OK
x-ratelimit-limit: 100
x-ratelimit-remaining: 87
x-ratelimit-reset: 1705312260| Header | Description |
|---|---|
x-ratelimit-limit | Maximum requests per window |
x-ratelimit-remaining | Requests remaining in the current window |
x-ratelimit-reset | Unix timestamp (seconds) when the window resets |
HTTP header names are case-insensitive; Krafter emits them in lowercase.
Exceeding the Limit
When the limit is exceeded, the API returns a 429 Too Many Requests response with a Retry-After header (RFC 7231 §7.1.3) and a JSON body that carries the same value as retry_after:
http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 23
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
x-ratelimit-reset: 1705312260json
{
"error": "Rate limit exceeded",
"retry_after": 23
}The number is the seconds remaining until the current window resets, not a back-off recommendation. Use either the header or the JSON field — they are always equal.
Handling Rate Limits
Exponential Backoff
The recommended approach is to honour Retry-After, then add jitter and exponential backoff for any further attempts:
typescript
async function sendWithRetry(payload: object, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch("https://app.krafter.dev/api/v1/emails", {
method: "POST",
headers: {
Authorization: "Bearer kr_live_abc123def456",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.status !== 429) {
return response;
}
const retryAfter = parseInt(response.headers.get("Retry-After") || "1", 10);
const jitter = Math.random() * 1000;
const delay = retryAfter * 1000 + jitter;
console.log(`Rate limited. Retrying in ${Math.round(delay / 1000)}s...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error("Max retries exceeded");
}Batch Sending
If you need to send to many recipients at once, use the batch endpoint instead of issuing individual requests. A single batch request counts as one request against your rate limit:
bash
curl -X POST https://app.krafter.dev/api/v1/emails/batch \
-H "Authorization: Bearer kr_live_abc123def456" \
-H "Content-Type: application/json" \
-d '{
"emails": [
{"from": "hello@yourdomain.com", "to": "alice@example.com",
"subject": "Welcome", "html": "<p>Hi Alice</p>", "stream": "transactional"},
{"from": "hello@yourdomain.com", "to": "bob@example.com",
"subject": "Welcome", "html": "<p>Hi Bob</p>", "stream": "transactional"}
]
}'Best Practices
- Watch
x-ratelimit-remainingand slow down before hitting zero. - Use the batch endpoint to send to multiple recipients in a single request.
- Honour
Retry-Afterbefore retrying on429. - Spread requests over time rather than sending bursts at the start of the window.
- Sandbox keys also have hourly and daily caps layered on top of the per-team rate limit. Hitting a sandbox cap returns
429with a policycode— see Error handling.