Rate limits and quotas
Per-team API rate limit and send quotas, and the variables that tune them.
API rate limit
Every team gets 10 requests per second, shared by all its keys. Set OPENSEND_RATE_LIMIT_PER_SEC to change it, or 0 to turn it off. The count lives in Postgres, so it holds across serverless instances.
Every response carries IETF headers:
ratelimit-policy: 10;w=1
ratelimit-limit: 10
ratelimit-remaining: 7
ratelimit-reset: 1Past the limit you get 429 rate_limit_exceeded with a retry-after header (seconds). Back off for that long, then retry. If the limit check itself errors, the request is allowed.
Send quotas
| Variable | Default | On breach |
|---|---|---|
OPENSEND_MONTHLY_EMAIL_LIMIT | 50000 | 429 monthly_quota_exceeded |
OPENSEND_DAILY_EMAIL_LIMIT | none (no daily cap) | 429 daily_quota_exceeded |
These are your own guardrails. They do not replace the SES quota. GET /usage shows what the team has used.
A team can also have its own daily_email_limit in the teams table. It overrides the variable. The daily and monthly windows are UTC calendar periods, and a scheduled email counts against the day it goes out, not the day you created it.
Automatic pause
To protect your SES reputation, a team is paused when its real sends reach either threshold over the window (24 hours by default):
- Bounces: 5% permanent bounces, with at least 100 recipients in the window.
- Complaints: 0.1% complaints, with at least 1000 recipients in the window.
The rate is events divided by recipients (to, cc and bcc) of the same set of emails. That set is the emails SES accepted whose send time falls in the window, so a scheduled email counts on the day it goes out. Simulator traffic (resend.dev) does not count.
| Variable | Default |
|---|---|
OPENSEND_AUTO_PAUSE | on; set 0 to turn it off |
OPENSEND_BOUNCE_PAUSE_RATE | 0.05 |
OPENSEND_COMPLAINT_PAUSE_RATE | 0.001 |
OPENSEND_GUARD_WINDOW_HOURS | 24 |
OPENSEND_GUARD_MIN_SENDS | 100 |
OPENSEND_GUARD_COMPLAINT_MIN_SENDS | 1000 |
What a paused team sees
- Every send path (single, batch, broadcasts, automations, inbox replies) returns 403
sending_paused, and the message includes the reason. - If the guard tables cannot be read, sends fail closed with 503
guardrail_unavailable. Retry shortly. It never reads as "not paused". - A broadcast or scheduled email that was already queued is held, not failed. It goes out after you unpause. Broadcasts are also held, then resumed, when they hit the daily or monthly quota.
Resume a team with:
pnpm exec tsx --env-file=.env scripts/unpause-team.mts <team_id>Scheduled times
A scheduled_at in the past means send now. It must be no more than 30 days ahead, or the request returns 422.
Broadcast speed
BROADCAST_SEND_RATE is emails per second for broadcasts. The default is 1. See Broadcasts.
Idempotency
Retrying a send with the same Idempotency-Key returns the stored response with idempotent-replayed: true instead of sending twice. Keys last 24 hours.