BETA
Skip to content

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 ​

ResourceLimitWindow
All API endpointsyour plan's API rate × 6060 seconds (fixed)

Free is 10 req/s, so 600 per window; Starter 50 req/s, Pro 100, Scale 500. Enterprise is not rate limited. See Rate limits → The default bucket follows your plan.

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
HeaderDescription
x-ratelimit-limitMaximum requests per window
x-ratelimit-remainingRequests remaining in the current window
x-ratelimit-resetUnix 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: 1705312260
json
{
  "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.

Monthly Send Quota ​

The per-minute limit above is not the only 429 you can get. Your plan also caps how many emails you may send per month, and the two are easy to confuse because they share a status code:

json
{ "error": "Rate limit exceeded", "retry_after": 23 }
json
{ "error": "Monthly usage limit exceeded. Upgrade your plan." }

The first clears in seconds. The second does not clear until the 1st of the next month — retrying it just burns requests.

Check how much is left at any time:

bash
curl https://app.krafter.dev/api/v1/usage \
  -H "Authorization: Bearer kr_live_abc123def456"
json
{
  "period_start": "2026-09-01T00:00:00Z",
  "period_end": "2026-10-01T00:00:00Z",
  "plan": "pro",
  "services": {
    "mail": {"metric": "emails_sent", "used": 9812, "limit": 10000, "remaining": 188, "unlimited": false}
  }
}

A mail:send or mail:read key is enough — it returns the mail entry and nothing else. Krafter also emails the team owner at 80% and 95% of the monthly limit, once per threshold per month.

If email is on your critical path

Sign-in codes, password resets and 2FA stop working the moment the monthly quota runs out, and no retry will bring them back before the month turns. Watch remaining and stop sending digests, reminders and other optional mail while there is still room for the mail you cannot do without.

See Rate limits → Monthly plan quota for the platform-wide view.

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-remaining and slow down before hitting zero.
  • Use the batch endpoint to send to multiple recipients in a single request.
  • Honour Retry-After before retrying on 429.
  • 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 429 with a policy code — see Error handling.

Built by Krafter Studio