Rate Limits
Understanding API rate limits and how to handle them gracefully.
Overview
Rate limits ensure fair usage and platform stability. SendMailOS uses a combination of soft limits (auto-demotion) and hard limits (429 errors).
Soft Limits
Transactional emails exceeding 5/sec are automatically demoted to marketing priority. No error is returned.
Hard Limits
Marketing emails exceeding 500/sec return a 429 error. You must wait before retrying.
Limits by Endpoint
| Endpoint | Rate Limit |
|---|---|
POST /send(Transactional) | 5 req/s (soft) |
POST /send(Marketing) | 500 req/s (hard) |
POST /campaigns/send | 5 req/min |
POST /subscribers | 100 req/s |
GET /subscribers | 100 req/s |
POST /domains | 10 req/min |
GET /domains | 60 req/min |
Transactional Auto-Demotion
When transactional emails exceed 5/sec, they're automatically queued as marketing priority instead of returning an error. This ensures delivery while protecting system capacity.
Rate Limit Headers
Every API response includes headers to track your rate limit status.
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when window resets |
Retry-After | Seconds to wait (only on 429 responses) |
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 423
X-RateLimit-Reset: 1705315860Handling Rate Limit Errors
When you exceed the hard limit, the API returns a 429 Too Many Requests response.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705315860
Retry-After: 30
{
"error": "Rate limit exceeded",
"message": "You have exceeded the maximum burst capacity (500/sec). Please slow down.",
"code": "RATE_LIMITED"
}Best Practices
1. Implement Exponential Backoff
Use the Retry-After header, or implement exponential backoff when retrying failed requests.
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise(r => setTimeout(r, delay));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}2. Monitor Rate Limit Headers
Track your remaining requests proactively to avoid hitting limits.
class RateLimitTracker {
remaining = Infinity;
resetTime = 0;
update(response) {
this.remaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '0'
);
this.resetTime = parseInt(
response.headers.get('X-RateLimit-Reset') || '0'
) * 1000;
}
async waitIfNeeded() {
if (this.remaining <= 1) {
const waitTime = this.resetTime - Date.now();
if (waitTime > 0) {
await new Promise(r => setTimeout(r, waitTime));
}
}
}
}3. Use a Queue for Bulk Operations
For sending many emails, use a queue library to control throughput.
import PQueue from 'p-queue';
// 10 requests per second
const queue = new PQueue({
intervalCap: 10,
interval: 1000,
carryoverConcurrencyCount: true
});
const emails = ['[email protected]', '[email protected]', /* ... */];
const results = await Promise.all(
emails.map(email =>
queue.add(() => client.emails.send({
to: email,
subject: 'Hello!',
html: '...'
}))
)
);Plan-Based Limits
Higher plan tiers include increased rate limits.
Starter
For small projects
- Transactional5 req/s
- Marketing500 req/s
Professional
For growing teams
- Transactional50 req/s
- Marketing1,000 req/s
Enterprise
Custom limits
- TransactionalCustom
- MarketingCustom