Skip to content
DocsRate limits

Rate limits

Send and authentication surfaces are rate-limited per workspace or login identifier. When you hit one the API returns 429 Too Many Requests with a Retry-After header in seconds.

Defaults

These are the platform defaults. The effective value can be lower during adaptive load shedding or different when a plan/workspace override applies.

Send window
60 / min, 1000 / hour

Both rolling windows apply; reaching either one can stop a send request.

Plan send cap
Per plan

Per-second provider/workspace limits and billing-period quota checks are separate gates.

Auth rate
5 / min, 20 / 15 min

Sensitive auth routes are checked by caller IP and, when present, normalized email.

Send limits are keyed by workspace ownership after authentication. Recipient-domain burst protection also checks every distinct domain in to, cc, and bcc. A plan quota error is not the same as a short request-rate window; inspect the HTTP status and error.code before deciding whether to retry.

Response headers

Use server-provided state instead of estimating the window on the client.

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 12
Content-Type: application/json

{
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Too many requests"
  }
}
X-RateLimit-Limit
Effective window

The active limit selected by the tightest evaluated window.

X-RateLimit-Remaining
Requests left

Never assume this is a global account balance or billing quota.

Retry-After
Seconds

Present on a limited response; wait at least this long before retrying.

Backoff

Retry only transient failures, with a finite budget and the same idempotency key.

async function sendWithBackoff(body, idempotencyKey) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch('https://sendix.dev/api/v1/emails', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.SENDIX_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return response.json();
    if (response.status !== 429 && response.status < 500) {
      throw new Error(`Sendix rejected the request: ${response.status}`);
    }

    const retryAfter = Number(response.headers.get('Retry-After') ?? 0);
    const baseMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
    const jitterMs = Math.floor(Math.random() * Math.min(baseMs, 1000));
    await new Promise((resolve) => setTimeout(resolve, baseMs + jitterMs));
  }
  throw new Error('Sendix retry budget exhausted');
}
The Node SDK already retries transient responses
By default, sendix-node performs two retries for 429 and 5xx responses. It honors Retry-After when present and otherwise uses exponential delay. Set maxRetries explicitly when your job has a stricter latency or retry budget.

Production design

Absorb bursts in your own queue

Put user-facing work onto a durable queue, acknowledge the user action, and let workers submit at a controlled concurrency. This prevents a traffic spike from turning into synchronized retries across every web process.

Classify before retrying

Retry 429, timeouts, connection failures, and selected 5xxresponses. Do not automatically retry validation, authentication, scope, domain, or quota failures. Record the response X-Request-Id so support can trace a failed call.

A retry is safe only when the operation is safe
GET requests are naturally repeatable. For email creation, persist one stable Idempotency-Key per business operation and reuse it across every attempt.