Docs/Rate Limits

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

EndpointRate Limit
POST /send(Transactional)5 req/s (soft)
POST /send(Marketing)500 req/s (hard)
POST /campaigns/send5 req/min
POST /subscribers100 req/s
GET /subscribers100 req/s
POST /domains10 req/min
GET /domains60 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.

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when window resets
Retry-AfterSeconds to wait (only on 429 responses)
http
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 423
X-RateLimit-Reset: 1705315860

Handling Rate Limit Errors

When you exceed the hard limit, the API returns a 429 Too Many Requests response.

http
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.

javascript
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.

javascript
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.

javascript
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

Related Resources