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"
}
}Paid Tiers
- Limit: None
- Note: Subject to abuse monitoring (see Anti-Abuse Measures)
Send Limits
By Tier
| Tier | Daily Limit | Burst Limit | Concurrency |
|---|---|---|---|
| Free | 0 (receive only) | N/A | N/A |
| Developer ($9/mo) | 1,000 messages | 10/minute | 5 concurrent |
| Growth ($49/mo) | 10,000 messages | 100/minute | 20 concurrent |
| Enterprise (custom) | 100,000+ messages | 1,000/minute | 100 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):
| Operation | Rate Limit | Scope |
|---|---|---|
| GET /v1/inboxes | 100/minute | Per account |
| GET /v1/messages | 200/minute | Per account |
| POST /v1/webhooks | 10/minute | Per account |
| GET /v1/threads | 100/minute | Per account |
| POST /v1/drafts | 50/minute | Per 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| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in current window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
X-RateLimit-Window | Window 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']}")