daimon.email
Security

Rate Limits

Request limits, quotas, and how to handle 429 responses

Overview

daimon.email enforces rate limits to ensure fair usage and prevent abuse. Rate limits vary by tier and operation type.

Info

All rate limits are enforced at the account level, not per inbox. Multiple inboxes under the same account share the same quotas.

Inbox Creation Limits

Free Tier

  • Limit: 10 inbox creates per hour per IP address
  • Scope: Global (across all free tier accounts from the same IP)
  • Reset: Rolling 1-hour window
  • Bypass: Upgrade to Developer tier or higher
// When rate limited:
{
  "error": "RATE_LIMIT_EXCEEDED",
  "code": "INBOX_CREATE_RATE_LIMIT",
  "retry_after": 3421,  // Seconds until oldest request expires
  "message": "Free tier limited to 10 inbox creates per hour. Next available slot in 3421 seconds.",
  "current_usage": {
    "count": 10,
    "window_start": "2026-03-16T14:00:00Z",
    "window_end": "2026-03-16T15:00:00Z"
  }
}

Send Limits

By Tier

TierDaily LimitBurst LimitConcurrency
Free0 (receive only)N/AN/A
Developer ($9/mo)1,000 messages10/minute5 concurrent
Growth ($49/mo)10,000 messages100/minute20 concurrent
Enterprise (custom)100,000+ messages1,000/minute100 concurrent

Note

Daily limits reset at midnight UTC. Burst limits are rolling 1-minute windows. Concurrency is the max simultaneous send operations.

Send Limit Response

// When you exceed daily limit:
{
  "error": "SEND_LIMIT_EXCEEDED",
  "code": "DAILY_SEND_QUOTA",
  "retry_after": 43200,  // Seconds until midnight UTC
  "message": "Daily send limit of 1,000 messages exceeded. Quota resets in 43200 seconds.",
  "current_usage": {
    "sent_today": 1000,
    "limit": 1000,
    "reset_at": "2026-03-17T00:00:00Z"
  },
  "upgrade_context": {
    "operator_action_url": "https://daimon.email/upgrade?token=...",
    "operator_action_label": "Upgrade to Growth tier for 10,000 sends/day",
    "agent_script": "Tell your operator: Daily send limit reached. Upgrade to Growth tier recommended."
  }
}

Burst Limit Response

// When you exceed burst limit (too many sends per minute):
{
  "error": "RATE_LIMIT_EXCEEDED",
  "code": "SEND_BURST_LIMIT",
  "retry_after": 45,  // Seconds until rate limit window expires
  "message": "Burst limit of 10 sends per minute exceeded. Retry in 45 seconds.",
  "current_usage": {
    "sends_this_minute": 11,
    "limit": 10,
    "window_start": "2026-03-16T14:23:00Z",
    "window_end": "2026-03-16T14:24:00Z"
  }
}

API Request Limits

General API operations (not inbox creation or sending):

OperationRate LimitScope
GET /v1/inboxes100/minutePer account
GET /v1/messages200/minutePer account
POST /v1/webhooks10/minutePer account
GET /v1/threads100/minutePer account
POST /v1/drafts50/minutePer account

Generic Rate Limit Response

{
  "error": "RATE_LIMIT_EXCEEDED",
  "code": "API_RATE_LIMIT",
  "retry_after": 12,
  "message": "Rate limit exceeded for GET /v1/messages. Retry in 12 seconds.",
  "rate_limit": {
    "limit": 200,
    "remaining": 0,
    "reset": "2026-03-16T14:24:00Z"
  }
}

Rate Limit Headers

Every API response includes rate limit information in headers:

HTTP/1.1 200 OK
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 157
X-RateLimit-Reset: 1710599040
X-RateLimit-Window: 60
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in current window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when the window resets
X-RateLimit-WindowWindow size in seconds
// Always check rate limit headers
const response = await fetch('https://api.daimon.email/v1/messages', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});

const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = parseInt(response.headers.get('X-RateLimit-Reset'));

if (remaining < 10) {
  console.warn(`Only ${remaining} requests left. Resets at ${new Date(reset * 1000)}`);
}
# Always check rate limit headers
import requests
import time

response = requests.get(
    'https://api.daimon.email/v1/messages',
    headers={'Authorization': f'Bearer {api_key}'}
)

remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
reset = int(response.headers.get('X-RateLimit-Reset', 0))

if remaining < 10:
    print(f"Only {remaining} requests left. Resets at {time.ctime(reset)}")

Handling 429 Responses

Exponential Backoff

Implement exponential backoff when you receive a 429 Too Many Requests response:

async function sendWithRetry(inbox_id, message, maxRetries = 3) {
  let attempt = 0;

  while (attempt < maxRetries) {
    try {
      return await client.inboxes.send(inbox_id, message);
    } catch (error) {
      if (error.status === 429) {
        const retryAfter = error.retry_after || Math.pow(2, attempt) * 1000;
        console.log(`Rate limited. Waiting ${retryAfter}ms before retry ${attempt + 1}/${maxRetries}`);

        await sleep(retryAfter);
        attempt++;
      } else {
        throw error;
      }
    }
  }

  throw new Error('Max retries exceeded');
}
import time

def send_with_retry(inbox_id, message, max_retries=3):
    attempt = 0

    while attempt < max_retries:
        try:
            return client.inboxes.send(inbox_id, message)
        except DaimonError as e:
            if e.status == 429:
                retry_after = e.retry_after or (2 ** attempt) * 1000
                print(f"Rate limited. Waiting {retry_after}ms before retry {attempt + 1}/{max_retries}")

                time.sleep(retry_after / 1000)
                attempt += 1
            else:
                raise

    raise Exception('Max retries exceeded')

Respecting retry_after

Always respect the retry_after value in error responses:

// BAD: Ignoring retry_after
try {
  await client.inboxes.send(inbox_id, message);
} catch (error) {
  if (error.status === 429) {
    await sleep(1000);  // Arbitrary 1 second delay
    await client.inboxes.send(inbox_id, message);  // Will likely fail again
  }
}

// GOOD: Using retry_after
try {
  await client.inboxes.send(inbox_id, message);
} catch (error) {
  if (error.status === 429) {
    await sleep(error.retry_after * 1000);
    await client.inboxes.send(inbox_id, message);
  }
}

Polling Best Practices

Use Webhooks Instead

Instead of polling GET /v1/messages every few seconds, use webhooks:

// BAD: Aggressive polling
setInterval(async () => {
  const messages = await client.messages.list(inbox_id);
  // Process messages
}, 2000);  // Every 2 seconds = 30 requests/minute

// GOOD: Webhook-driven
await client.webhooks.create({
  url: 'https://your-server.com/webhooks/daimon',
  events: ['message.received'],
  inbox_id: inbox_id
});

// Your webhook handler receives messages instantly
app.post('/webhooks/daimon', (req, res) => {
  const message = req.body.data;
  // Process message
  res.sendStatus(200);
});

Smart Polling

If you must poll (e.g., local development without public URL):

// Adaptive polling with exponential backoff
let pollInterval = 5000;  // Start at 5 seconds
const maxInterval = 60000;  // Max 1 minute
const minInterval = 5000;   // Min 5 seconds

async function pollMessages() {
  const messages = await client.messages.list(inbox_id);

  if (messages.length > 0) {
    // Messages found, reset to fast polling
    pollInterval = minInterval;
    processMessages(messages);
  } else {
    // No messages, slow down
    pollInterval = Math.min(pollInterval * 1.5, maxInterval);
  }

  setTimeout(pollMessages, pollInterval);
}
# Adaptive polling with exponential backoff
import time

poll_interval = 5  # Start at 5 seconds
max_interval = 60  # Max 1 minute
min_interval = 5   # Min 5 seconds

def poll_messages():
    global poll_interval

    messages = client.messages.list(inbox_id)

    if messages:
        # Messages found, reset to fast polling
        poll_interval = min_interval
        process_messages(messages)
    else:
        # No messages, slow down
        poll_interval = min(poll_interval * 1.5, max_interval)

    time.sleep(poll_interval)
    poll_messages()

Enterprise Custom Limits

Enterprise customers can request custom limits:

  • Higher send quotas (100K+ per day)
  • Dedicated IP addresses (no shared limits)
  • Custom burst limits for time-sensitive campaigns
  • Allowlist bypass for trusted integrations

Contact sales@daimon.email with:

  • Current tier and usage
  • Desired limits and justification
  • Expected growth over 6 months

Monitoring Your Usage

Check your current usage via the capabilities endpoint:

const caps = await client.account.capabilities();

console.log('Send usage:', caps.usage.sends_today, '/', caps.limits.sends_per_day);
console.log('Inbox count:', caps.usage.inboxes, '/', caps.limits.max_inboxes);
caps = client.account.capabilities()

print(f"Send usage: {caps['usage']['sends_today']} / {caps['limits']['sends_per_day']}")
print(f"Inbox count: {caps['usage']['inboxes']} / {caps['limits']['max_inboxes']}")

Additional Resources