Error reference

rate_limited

HTTP 429 retryable: true REST and MCP

A request-rate limit refused this call, and the response carries Retry-After so you know how long to wait.

Why it happens

There are two different kinds of limit behind this code and they behave differently. The general one is a request-rate throttle with three buckets: an edge bucket keyed on a hash of the client IP, a workspace bucket, and a principal bucket keyed by api key, connection or identity. Only verified principal fields feed the last two, never a workspace header, so a caller cannot spread its load across buckets by claiming a different workspace. The other kind is a per-operation cooldown on things that are expensive or that send mail of their own, and those have fixed, much longer windows.

Every condition below was read in the implementation at the file and line given. They are the branches that raise this code, not a summary of what the name suggests.

How to fix it

  1. Honour Retry-After. The header is set to 60 on every rate_limited response from both the REST API and the MCP endpoint.
  2. Back off exponentially with jitter rather than retrying on a fixed interval. This is the one family of errors where retrying is correct, which makes a synchronised retry storm the realistic failure.
  3. Spread work across time rather than across credentials. Buckets are keyed on the verified principal and on the workspace, so a second key in the same workspace shares the workspace bucket.
  4. For exports, batch the request. Two per hour per member is the cap, and a wider single export is cheaper than several narrow ones.

Why it works this way

The edge counter is treated as permissive and never as billing authority. It is local to a Cloudflare location and therefore cannot see global traffic, so it is used to shed obvious load early and the real accounting happens on the principal and workspace buckets behind it.

Messages you will see

The message field is written for a human reading a log and is not a contract value. Branch on code instead. These are the exact strings this code is sent with today, listed so that a search for one of them lands here.

Where it appears

These are the documented surfaces that raise it. The REST base is https://api.emailforagents.ai/v1, and the remote MCP endpoint is https://mcp.emailforagents.ai/mcp.

The response

Read request_id off the body or the x-request-id header before doing anything else. It is the only handle on one specific attempt, and for a 503 it is the only handle at all, because the underlying detail stays server-side.

HTTP/1.1 429
content-type: application/json
x-request-id: req_...
retry-after: 60

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry shortly.",
    "request_id": "req_...",
    "retryable": true
  }
}

Verified against packages/domain/src/request-guards.ts:113. The status comes from ERROR_STATUS in packages/contracts/src/errors.ts, which is the one place a code is mapped to a status.

All error codes · Documentation · Quickstart