Rate limits
Which endpoints are rate limited, the limits, and the 429 response.
Most of the API has no request rate limit. Limits apply to sign-in, agent registration, human claims and sends from unclaimed sandbox agents. There are no X-RateLimit-* headers.
Note
The OpenAPI document lists a 429 response on almost every endpoint. Only the endpoints on this page return it today. Limits may be added to other endpoints later, so handle 429 everywhere.
The 429 response
Retry-After is in seconds. It is sent by the sign-in, agent registration and sandbox send limits; the claim limit returns 429 without it. Agent registration uses the OAuth error body instead of the envelope.
Sandbox sends
An unclaimed anonymous agent registration (see Authentication) has three caps on its sandbox inbox. The numbers below are deployment defaults: MachineMail can change them at any time, and every error states the cap in force (so does auth.md).
| Cap | Default | Over it |
|---|---|---|
| Messages per rolling 24 hours | 10 | 429 rate_limited with Retry-After |
| Different recipients per rolling 24 hours | 10 | 429 rate_limited with Retry-After |
| Recipients per message | 5 | 400 validation with param: "to" (retrying never helps) |
Recipients are to, cc and bcc together, compared by bare address and case-insensitively. Writing again to an address the inbox already wrote to in the last 24 hours does not count as a new recipient, so replies in an ongoing conversation keep working. The caps cover:
POST /v0/inboxes/{inbox_id}/messages/sendPOST /v0/inboxes/{inbox_id}/messages/{message_id}/replyPOST /v0/inboxes/{inbox_id}/messages/{message_id}/reply-allPOST /v0/inboxes/{inbox_id}/messages/{message_id}/forwardPOST /v0/inboxes/{inbox_id}/drafts/{draft_id}/send, and scheduled drafts- the same routes under
/v0/pods/{pod_id}/..., and the MCP send, reply, forward and draft tools
Draft create and update check the per-message cap too. With the defaults, the 11th message in the window returns:
Retry-After is the time until enough of the inbox’s sends are 24 hours old for the next one to fit. Once a person claims the agent, the inbox leaves the sandbox and none of these caps apply.
Agent registration
POST /agent/identity is limited per requester and across the whole service. A requester is an IPv4 address, or an IPv6 /64 (one subscriber usually holds a whole /64).
| Registration type | Per requester | Whole service |
|---|---|---|
anonymous, service_auth | 3 per rolling hour and 10 per rolling 24 hours (deployment defaults) | 100 per hour |
identity_assertion | 60 per hour | 1000 per hour |
The anonymous and service_auth per-requester caps count stored registrations over rolling windows, on every MachineMail machine, and are platform limits MachineMail can change at any time (the error states the cap in force). Retry-After is the time until the oldest counted registration leaves the window. The other caps are token buckets that refill evenly over an hour; their Retry-After is the time for one token to refill: 36 seconds for the anonymous service bucket, 60 and 4 seconds for the identity assertion buckets.
When the service-wide bucket is empty the message is Registration capacity exhausted; retry later.
Token polling
While waiting for a human to finish a claim, poll POST /oauth2/token with the claim grant no more than once every 5 seconds. Faster polling returns 400 with error: slow_down.
Human claims
Signed-in people can look up or confirm agent claims (GET and POST /auth/claim) 10 times per 10 minutes. Every lookup counts, not only wrong codes. The 11th returns 429 Too many claim attempts... without Retry-After.
Dashboard sign-in
These limits protect the /auth/* endpoints used by the dashboard. They are token buckets: a burst up to the capacity, then a steady refill.
| Bucket | Burst | Refill | Applies to |
|---|---|---|---|
| Per client IP (one bucket for all of them) | 20 | 1 every 6 seconds (10 per minute) | POST /auth/login, POST /auth/signup, POST /auth/magic-link, POST /auth/email/start, POST /auth/email/verify, POST /auth/pending/organization, POST /auth/pending/verify |
| Per email, sign-in | 5 | 1 per minute | POST /auth/login (reset after a successful sign-in) |
| Per email, sign-up | 5 | 1 per minute | POST /auth/signup |
| Per email, sign-in link | 5 | 5 per 15 minutes | POST /auth/magic-link |
| Per email and client IP, sign-in code | 5 | 5 per 15 minutes | POST /auth/email/start (with only an invitation_token, per token) |
| Per email and client IP, code check | 5 | 1 per minute | POST /auth/email/verify, POST /auth/pending/verify (reset after a successful check) |
A code also stops working after 5 wrong tries: send a new one. On top of the buckets, a client IP that enters 20 wrong codes for one address in 24 hours gets no new code and no code check for that address (429) until the oldest of those is a day old; this count is kept in the database, not per machine, and the person is emailed. An IPv6 client counts by its /64.
New codes are also limited in the database, so across every API machine. Email sign-in codes (POST /auth/email/start) and the verification codes of social sign-ins whose email the provider does not vouch for (GET /auth/callback going to /login/verify) count together:
| In the database | Limit |
|---|---|
New codes per address and client IP (/64) | 5 per 15 minutes |
| New codes per address, from any IP | 10 per rolling hour |
Past these, POST /auth/email/start answers 429 with Retry-After, and a social sign-in goes to /login?error=codes_paused without sending a code.
Nothing keyed on the address alone refuses a code check, so someone guessing codes for your address cannot lock you out: their own network is paused, and your right code from anywhere else still works. Wrong codes never hold back new codes for anyone but the network that entered them. Per address, the limits only delay new codes; a code you already have keeps working. Guessing stays bounded even for someone who keeps changing IP address, because what bounds it is the number of codes: each code allows 5 attempts, and an address gets at most 10 new codes an hour whatever the IP. That is at most 50 guesses an hour per address (1,200 a day, about a 0.12% chance a day against a one-in-a-million 6-digit code). Our sign-in service keeps its own limits on top (3 codes per minute and 10 checks per minute per address); past those, the API also answers 429 with Retry-After. The trade-off: to keep you from getting a new code, someone has to keep asking for 10 codes an hour for your address (that cannot be told apart from you asking). Meanwhile a code you already have still works, and social sign-in is unaffected. A CAPTCHA (Cloudflare Turnstile) on code requests is the planned follow-up.
Every bucket that applies is charged on every request, even when another bucket already refused it. Email keys are trimmed and lowercased. Behind Fly.io’s proxy the client IP is taken from Fly-Client-IP, then the first X-Forwarded-For address.
Notes
- Limits are kept in memory on each API machine. With several machines, the effective limit is the per-machine limit times the number of machines that serve your requests.
- Webhook deliveries to your endpoint are not rate limited by MachineMail; see Webhook events for the retry schedule.