Sendr Docs
Guides

Rate Limits & Retries

Understand Sendr's rate limits and how to implement retry strategies.

Rate limit types

Sendr enforces several types of limits:

Limit typeScopeError code
Request ratePer API keyrate_limited (429)
Daily email quotaPer organizationdaily_limit_reached (429)
Monthly email quotaPer organizationmonthly_limit_reached (429)
Overage capPer billing periodoverage_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):

PlanMonthlyDaily
Free3,000100
Pro50,000Unlimited
Business200,000Unlimited
EnterpriseUnlimitedUnlimited

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.

On this page