rate limits.md
title: "Rate limits" description: "Which MachineMail endpoints are rate limited (sign-in, agent registration, claims, sandbox sends, MCP feedback), their limits and the 429 response." canonical: "https://machinemail.dev/docs/reference/rate-limits"
Rate limits
Which MachineMail endpoints are rate limited (sign-in, agent registration, claims, sandbox sends, MCP feedback), their limits and the 429 response.
Source: https://machinemail.dev/docs/reference/rate-limits
Most of the API has no request rate limit. Limits apply to sign-in, agent registration, human claims, sends from unclaimed sandbox agents and MCP feedback. There are no X-RateLimit-* headers.
Note: The OpenAPI document lists a
429response on almost every endpoint. Only the endpoints on this page return it today. Limits may be added to other endpoints later, so handle429everywhere.
The 429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 36
Content-Type: application/json
{"error":{"type":"rate_limited","message":"Too many requests. Try again later."}}
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:
| Cap | Limit | 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. The 11th message in the window returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 5400
{ "error": { "type": "rate_limited", "message": "Sandbox inboxes can send 10 messages per 24 hours. Have your operator claim this inbox to lift sandbox limits (see /auth.md)." } }
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). IPv6 requesters are also counted by their /48: at most 10 anonymous and service_auth registrations per rolling 24 hours from all the /64s in one /48 together.
| Registration type | Per requester | Whole service |
|---|---|---|
anonymous, service_auth |
1 per rolling hour and 3 per rolling 24 hours | About 100 per rolling hour and 1000 per rolling 24 hours |
identity_assertion |
60 per hour | 1000 per hour |
anonymous registrations also need a free place among the unclaimed sandboxes: about 500 can be live across the service at once. A sandbox holds its place until it is claimed, revoked or its 24-hour claim window ends.
The anonymous and service_auth caps, per requester and across the service, and the unclaimed sandbox cap count registrations over rolling windows. Retry-After is the time until enough counted registrations leave the window (for unclaimed sandboxes: until the oldest one's claim window ends; a claim can free a place sooner). A registration that fails is not counted. The service-wide caps are approximate: registrations from many requesters at the same moment can pass them together. The identity_assertion caps are token buckets that refill evenly over an hour; their Retry-After is the time for one token to refill: 60 and 4 seconds.
{
"error": "rate_limited",
"error_description": "Too many registrations from this address (at most 1 per hour); retry later",
"message": "Too many registrations from this address (at most 1 per hour); retry later"
}
When the service-wide cap is reached the message is Registration capacity exhausted; retry later, and when the unclaimed sandboxes are full it is Sandbox capacity is full right now; 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, POST /auth/passkey/verify |
| Per client IP, passkey options | 20 | 1 every 6 seconds (10 per minute) | POST /auth/passkey/options |
| 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 3 wrong tries: send a new one. Right codes are limited too: 4 per address per minute, whatever the IP (429 with Retry-After; the same code works once the minute is up). Wrong codes never count toward that, so nobody guessing can use it up. 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, and the person is emailed. An IPv6 client counts by its /64.
New codes are also limited per address. 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:
| Code requests | 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 3 attempts, and an address gets at most 10 new codes an hour whatever the IP. That is at most 30 guesses an hour per address (720 a day, about a 0.07% chance a day against a one-in-a-million 6-digit code). We check every code ourselves before it goes any further, so wrong guesses never use up the address's sign-in allowance: only a right code can, and right codes are capped at 4 a minute per address to stay well inside it. 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.
Passkey sign-ins have their own allowance: 4 per address a minute (429 with Retry-After). It is apart from email codes both ways: asking for or guessing codes never holds up a passkey, and a passkey sign-in never counts as a code try. Codes to confirm a sensitive change in Settings → Security are limited to 3 an hour per person, 3 tries each.
Every bucket that applies is charged on every request, even when another bucket already refused it.
Limits per email or per address count per mailbox: the address is trimmed and lowercased, everything from the first + of the local part is dropped, and for Gmail (gmail.com, googlemail.com) the dots are removed too. So k.yle+test@gmail.com and kyle@googlemail.com share the buckets and limits of kyle@gmail.com, and spelling an address another way buys no more codes or attempts.
MCP feedback
The MCP send_feedback tool has two token buckets, per organization (for an unclaimed sandbox agent, per registration):
| Bucket | Burst | Refill |
|---|---|---|
| Per hour | 20 | 1 about every 3 minutes |
| Per day | 100 | about 4 an hour (100 in 24 hours) |
Unclaimed sandbox agents also share one bucket of 200 an hour between them. Over a limit, the tool result is an isError rate_limited error with retry_after (seconds) in structuredContent.error. A call that fails on our side does not count. Both buckets are charged on every call. The limits apply per server machine, so a few more calls can get through when several machines answer.
Notes
- Webhook deliveries to your endpoint are not rate limited by MachineMail; see Webhook events for the retry schedule.