Rate limits
Every Bird API endpoint is rate limited. Limits exist to keep the platform stable for everyone; they are generous enough that well-behaved integrations rarely hit them, and every response tells you exactly where you stand so you never have to guess.
This page is the one home for rate limits: the model, which is the same across all channels, and the numbers, with every group's base rate in the tables below.
How limits are keyed
Rate limits are resolved per organization: every API key and workspace in the organization draws against the same resolved allowance. How that allowance is bucketed depends on the group:
- Send groups (email_send, sms_send, and the other per-product send and batch groups) share one organization-wide bucket. They are account quotas: it doesn't matter which key or workspace sends.
- Management groups (read, list, write) bucket per acting credential within the organization, so one runaway script can't drain the whole organization's read budget.
- Unauthenticated endpoints (login, signup, password reset) are keyed by client IP, with fixed abuse-prevention thresholds that are not adjustable.
Groups
Endpoints are grouped into rate limit groups, and the group sets the limit. A group covers endpoints with a similar cost profile: single-resource reads share the read group, collection queries list, management writes write, and expensive operations get groups of their own (email_send, email_batch, sms_send, whatsapp_send, verify_send, and so on). When you hit a limit, only that group is exhausted; running out of send quota does not block you from reading delivery status or managing webhooks.
The three management groups span every product and start at the same base rate for all organizations:
| Group | Base rate | Covers |
|---|---|---|
| list | 150/min | Collection queries: list messages, events, recipients, domains, suppressions. |
| read | 500/min | Single-resource lookups: a message by ID, a domain, a webhook, a suppression. |
| write | 60/min | Creates, updates, and deletes: domains, webhooks, suppressions. |
The management buckets count per acting credential within your organization, so one busy worker's polling can't starve another key's reads. Send buckets are org-wide account quotas, one per product, and every organization starts from the same one-minute-window base rates:
| Group | Base rate | Covers |
|---|---|---|
| email_send | 10/min | POST /v1/email/messages, plus mailbox sends (replies and forwards). |
| email_batch | 5/min | POST /v1/email/batches (one request queues up to 100 messages), plus contact batch imports. |
| sms_send | 10/min | POST /v1/sms/messages. |
| sms_batch | 5/min | POST /v1/sms/batches, up to 100 messages per request. |
| whatsapp_send | 10/min | POST /v1/whatsapp/messages. |
| verify_send | 10/min | POST /v1/verify/verifications (sending or resending a code). |
| verify_check | 60/min | POST /v1/verify/verifications/check. |
Three things to know about the send buckets. They count requests, not recipients, which makes batching the volume lever: at base rates, single email sends top out at 10 messages a minute while 5 batch calls queue 500 in the same window, the bulk-sending path for email and SMS. whatsapp_send is an abuse guard on request volume, not a spend control: WhatsApp is billed per message, so your wallet governs spend. And the verify groups bound your account's request volume only; the per-recipient guardrails that throttle code requests and guesses are separate, documented under abuse guardrails.
The group that matched your request is named in the rate-limit response headers, so you always know which bucket you exhausted.
Treat every number on this page as a starting point, not a constant to build against. Limits change: your plan raises them, a per-organization override replaces them (see How your limit is resolved), and base rates may be tuned over time. Your effective quota is always advertised on the response itself, so a well-built client discovers its limits from live traffic rather than hardcoding the values above.
How your limit is resolved
Your effective limit for a group is resolved from three layers:
- Base: the default that applies to every organization.
- Plan: your subscription plan raises the limit for specific groups.
- Override: a per-organization override, arranged with Bird, for customers whose volume outgrows their plan's ceiling. An active override replaces the base and plan values.
In practice the base is where every organization starts, your plan raises it, and an override is the escape hatch when you need more. If your traffic is approaching your plan's ceiling, contact support; overrides are a normal part of scaling on Bird, not an exception process. Your current effective quota for a group is advertised on every response in the RateLimit-Policy header, so you can read it from live traffic.
Response headers
Every response from a rate-limited endpoint carries two headers in the IETF Structured Fields format (RFC 9651):
Codebeispiel
RateLimit-Policy: "email_send";q=1000;w=60
RateLimit: "email_send";r=842;t=35| Header | Meaning |
|---|---|
| RateLimit-Policy | The policy that applies: q is the quota (max requests) and w is the window in seconds. |
| RateLimit | Your current state: r is the number of requests remaining and t is the seconds until the window resets. |
The quoted string names the group that matched the request. In this example, the org has an effective email_send limit of 1000 requests per 60 seconds, with 842 remaining and 35 seconds until reset.
Pace yourself against r and t proactively rather than only reacting to 429s: a client that slows down as r approaches zero never gets throttled at all. Bird uses this format instead of the older X-RateLimit-* convention because it is unambiguous (t is always seconds from now, never a Unix timestamp) and because a single response can express multiple policies if an endpoint is ever subject to more than one.
When you hit a limit
Rate-limit handling belongs in every production integration: a 429 is a normal, expected response, not an exceptional failure. At minimum, your client should honor Retry-After and retry with backoff; a client that also paces itself against the live RateLimit headers (see Response headers) avoids being throttled at all.
An exhausted limit returns 429 Too Many Requests with a Retry-After header (in seconds, matching the t value) and both RateLimit headers, with r=0 and the name of the group you exhausted. The body is the standard error envelope; this one is a real response:
Codebeispiel
{
"error": {
"type": "rate_limit_error",
"code": "E01003",
"name": "RateLimited",
"message": "Too many requests. Please retry after the period indicated in the Retry-After header.",
"doc_url": "https://bird.com/docs/api/errors/E01003",
"request_id": "req_01ky7qavkff7qr88vadv6bv948"
}
}Branch on type: rate_limit_error, not the message text. The group name and retry timing are in the headers, not the body:
Codebeispiel
import time
import requests
def send_with_backoff(url, headers, payload, max_attempts=5):
for attempt in range(max_attempts):
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 429:
return response
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
raise RuntimeError("rate limited after max retries")A rate-limited request is rejected before it does any work: it does not consume an idempotency key, so retrying with the same key is always safe.
The Bird SDKs handle all of this automatically: they detect 429s, honor Retry-After, and retry with backoff, so most SDK users never write this code.
Failure mode
The rate limiter fails open: if its backing store is unavailable, requests are allowed rather than spuriously refused. Rate limiting is a protective measure, not a security boundary; authentication and authorization are always enforced regardless of the limiter's health. You will never receive a 429 caused by a Bird-side outage.
Next steps
- Errors: the error envelope and how to branch on error types
- Idempotency: safe retries for mutating requests
- SDK concepts: automatic retry and backoff behavior