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.
Both rolling windows apply; reaching either one can stop a send request.
Per-second provider/workspace limits and billing-period quota checks are separate gates.
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"
}
}The active limit selected by the tightest evaluated window.
Never assume this is a global account balance or billing quota.
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');
}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.
Idempotency-Key per business operation and reuse it across every attempt.
