Rate Limits & Retries
Understand Sendr's rate limits and how to implement retry strategies.
Rate limit types
Sendr enforces several types of limits:
| Limit type | Scope | Error code |
|---|---|---|
| Request rate | Per API key | rate_limited (429) |
| Daily email quota | Per organization | daily_limit_reached (429) |
| Monthly email quota | Per organization | monthly_limit_reached (429) |
| Overage cap | Per billing period | overage_limit_reached (429) |
Request rate limits
The API enforces a request rate limit per API key. The default limit is 100 requests per 10 seconds. When exceeded, the API returns:
HTTP 429 Too Many Requests
{
"statusCode": 429,
"code": "rate_limited",
"message": "Rate limit exceeded. Slow down and try again."
}
The response includes a Retry-After header indicating how many seconds to wait.
Email quotas
Your plan's monthly and daily limits apply to the number of emails delivered (not API requests):
| Plan | Monthly | Daily |
|---|---|---|
| Free | 3,000 | 100 |
| Pro | 50,000 | Unlimited |
| Business | 200,000 | Unlimited |
| Enterprise | Unlimited | Unlimited |
Pro and Business plans support overage — additional emails are billed per 1,000. The hard cap is 3× the monthly quota.
Implementing retries
TypeScript SDK (built-in)
The SDK retries 5xx errors automatically. Configure with the retries option:
const sendr = new Sendr({
apiKey: process.env.SENDR_API_KEY!,
retries: 3, // retry up to 3 times (default: 2)
});
For 429 errors (rate limits), use the retryAfter value from the error:
import { RateLimitError } from "sendr";
async function sendWithRetry(payload: SendEmailRequest, maxAttempts = 5) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await sendr.emails.send(payload);
} catch (error) {
if (error instanceof RateLimitError) {
const waitMs = (error.retryAfter ?? 1) * 1000;
console.log(`Rate limited. Waiting ${waitMs}ms before retry ${attempt}`);
await new Promise(resolve => setTimeout(resolve, waitMs));
continue;
}
throw error; // Non-retryable error
}
}
throw new Error("Max retry attempts exceeded");
}
Exponential backoff
For robust retry logic, use exponential backoff with jitter:
function exponentialBackoff(attempt: number, base = 500, max = 30000): number {
const delay = Math.min(base * Math.pow(2, attempt), max);
// Add ±20% jitter to prevent thundering herd
const jitter = delay * 0.2 * (Math.random() * 2 - 1);
return Math.floor(delay + jitter);
}
async function sendWithBackoff(
payload: SendEmailRequest,
maxAttempts = 5
): Promise<EmailResponse> {
let lastError: Error;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await sendr.emails.send(payload);
} catch (error) {
lastError = error as Error;
if (error instanceof RateLimitError) {
// Use server-provided retry time if available
const delay = error.retryAfter
? error.retryAfter * 1000
: exponentialBackoff(attempt);
await sleep(delay);
continue;
}
// 5xx errors are transient — retry with backoff
if (error instanceof ApiError && error.statusCode >= 500) {
await sleep(exponentialBackoff(attempt));
continue;
}
// 4xx errors (except 429) are not retryable
throw error;
}
}
throw lastError!;
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
Python
import time
import random
from sendr.exceptions import RateLimitError, ApiError
def exponential_backoff(attempt: int, base: float = 0.5, max_delay: float = 30.0) -> float:
delay = min(base * (2 ** attempt), max_delay)
jitter = delay * 0.2 * (random.random() * 2 - 1)
return delay + jitter
def send_with_retry(client, payload: dict, max_attempts: int = 5):
last_error = None
for attempt in range(max_attempts):
try:
return client.emails.send(**payload)
except RateLimitError as e:
delay = e.retry_after if e.retry_after else exponential_backoff(attempt)
time.sleep(delay)
last_error = e
except ApiError as e:
if e.status_code >= 500:
time.sleep(exponential_backoff(attempt))
last_error = e
else:
raise # 4xx is not retryable
raise last_error
Batch sending to stay under rate limits
For high-volume sends, use the batch endpoint to send up to 100 emails per request:
// Instead of 100 individual requests:
for (const user of users) {
await sendr.emails.send({ to: user.email, ... }); // slow!
}
// Send them all in one batch request:
await sendr.emails.sendBatch({
from: "hello@acme.com",
emails: users.map(user => ({
to: user.email,
html: `<p>Hello ${user.name}!</p>`,
})),
});
For very large lists (thousands of emails), use Campaigns instead — they handle pagination and queuing automatically.
Monitoring usage
Check your current usage programmatically:
const usage = await fetch("https://api.sendr.dev/v1/billing/usage", {
headers: { Authorization: `Bearer ${process.env.SENDR_API_KEY}` },
}).then(r => r.json());
console.log(`${usage.emails_sent} / ${usage.emails_limit} emails used`);
console.log(`${usage.usage_percent}% of monthly quota`);
Sendr also sends a warning email when you reach 80% of your monthly quota.