Rate limits here are anti-abuse guardrails on the control plane, not usage quotas on your work. Nothing on this page meters how many issues the agent triages or how many CI minutes you burn, those are covered by your seat, and your only variable cost is your own token bill (see BYOK).
What is limited is how fast a client may call the API.
The buckets
Three sliding windows, all one minute wide.
| Bucket | Keyed on | Budget |
|---|---|---|
| Reads | Your token or session | 60 / minute |
| Writes, per conversation | Your token or session, and the conversation | 10 / minute |
| Writes, per token | Your token or session | 60 / minute |
| Pre-auth | Caller IP | 300 / minute |
The per-IP bucket is applied before your credential is resolved. That is deliberate: it bounds credential probing and keeps an unauthenticated flood from reaching the database at all.
Read and write are separate budgets, not a shared pool. Spending your writes does not stop you polling.
Why writes have two numbers
A write is charged twice: once to the conversation it belongs to, once to the token overall. The narrow bucket is checked first, so one busy conversation cannot drain the budget of the others beside it.
Naming a conversation is how you get your own fair share. Today the only surface
that does is MCP: pass conversation_ref as an argument and that conversation
gets its own 10 a minute, up to the token ceiling of 60. Everything else shares
one anonymous conversation, so its effective limit is the 10: every REST write,
and any MCP call that does not carry the argument.
Because two buckets can bind, the RateLimit-* headers report whichever one runs
out first: seeing 10 on one write and 60 on the next is correct, not a bug.
What counts as a write
By default, the class is the HTTP method: POST, PUT, PATCH and DELETE
are writes, everything else is a read.
MCP is the exception. Every MCP call is a single POST /v1/mcp, so charging
them all to the 10-per-minute write bucket would rate-limit read verbs like
box list almost immediately. MCP requests are classified by the verb's scope
instead: a read verb spends from the read budget.
Endpoints with their own limit
| Endpoint | Limit |
|---|---|
GET /v1/public/oss/:owner/:repo/stats |
60 / minute per IP |
GET | POST /unsubscribe |
30 / minute per IP |
POST /v1/runners/hardening is not rate-limited. It is our own runner code
reporting once per install, and several boxes can share one egress IP behind
NAT, and a per-IP cap there would refuse a legitimate box rather than a prober.
The bearer is checked before any lookup, so it is not a flood surface either.
Watching a run without spending your whole read budget
A 60-per-minute read budget and a once-a-second poll are the same number. Watch
one agent run at 1 Hz and you have spent 100% of your reads on it, with nothing
left for anything else, and a fleet rollout (box update) runs for minutes to
hours.
So the run tail takes a long poll. Send wait_ms and the request is held
open until the first new event lands, up to 25 seconds, and answers the instant
one does:
curl "https://api.developerz.ai/v1/tasks/$TASK/events?since_seq=$CURSOR&wait_ms=25000" \
-H "Authorization: Bearer dev_pat_…"
{ "run_id": "run_…", "events": [], "next_since_seq": 412 }
Over MCP it is the same knob on the same capability:
{ "action": "run_tail", "run_id": "run_…", "since_seq": 412, "wait_ms": 25000 }
| Watching one run for an hour | Requests spent |
|---|---|
| polling every second | 3600, and every minute of it is your entire read budget |
wait_ms=25000 on an idle run |
about 144, roughly 4% of the budget |
Five rules worth knowing before you build on it:
- An empty page after a wait is not an error. It means nothing happened in
that window. Re-send with the same cursor.
next_since_seqechoes your own cursor back for exactly this reason, so a caught-up watcher never rewinds to the head of the run. - Omit it, or send
0, and nothing changes. The call answers immediately, exactly as it did before the knob existed. - A refusal is never held. An unknown task, or a run that is not yours, is
answered straight away rather than after the wait. The shape depends on the
transport: over REST it is a
404with a problem+json body, and over MCP it is a tool result flaggedisErrornaming the run, because a JSON-RPC result carries no HTTP status. - A run that already ended settles the wait too. Once its final event has
landed nothing can arrive after your cursor, so the call answers an empty page
at once instead of holding the window open. That is your cue to stop: read
GET /v1/tasks/$TASK(or MCPtask run_status) and you will see the run finished. - Above 25000 is refused, not clamped. A long poll writes nothing until it answers, so it cannot survive the 30-second idle timeouts common to proxies and HTTP clients the way a streaming connection can.
Why a long poll and not a bigger budget
The other way to fix this would be a third bucket just for watching. We did not build one, on purpose: it would hand you more requests to spend without making any of them carry more, we would still answer 60 empty reads a minute per watcher, and you would have another budget to reason about. Fewer requests that each carry an answer is the better trade for both sides. If you are an agent, it is not close: every poll is a tool call whose result lands in your context, so 60 empty answers a minute is a context bill long before it is a rate-limit one.
It is a POST, and it always will be
There is no server-push transport on the MCP surface. Every MCP call is one
JSON-RPC POST /v1/mcp and a tool result is one message, so a verb cannot hold a
stream open. wait_ms is the whole answer there, and it is not a stopgap for a
subscription that is coming later.
REST has both shapes, because they are two different capabilities rather than one
answer in two envelopes. GET /v1/tasks/:id/events is the page you poll;
GET /v1/fleet/runs/:runId/events is the Server-Sent Events stream you subscribe
to, resumable with Last-Event-ID. Use the stream if you can hold a connection
open. Use wait_ms if you cannot, which includes every agent talking to us over
MCP.
Every response tells you what is left
You do not have to count your own calls, and you should not have to get throttled
to discover the cap. Every /v1 response produced behind the auth guard carries
three headers:
HTTP/1.1 200 OK
RateLimit-Limit: 60
RateLimit-Remaining: 58
RateLimit-Reset: 41
| Header | Means |
|---|---|
RateLimit-Limit |
the budget of the bucket this request was charged against |
RateLimit-Remaining |
what is left in it after this request |
RateLimit-Reset |
seconds until a slot frees, never a timestamp |
Two things follow from "the bucket this request was charged against":
- A read and a write report different limits, because they are different budgets.
Seeing
60on aGETand10on thePOSTright after it is correct. - On
POST /v1/mcpthe numbers follow the verb, not the method, exactly the way the bucket does.tools/listreports the read budget.
A request that fails to authenticate reports the pre-auth per-IP bucket instead, because that is the only one it spent.
Two /v1 endpoints are mounted ahead of the guard and send no RateLimit-*
headers at all: GET /v1/openapi.json and the public OSS stats badge. Both are
publicly cacheable, so a per-caller counter sitting behind a shared cache would
report a wrong number to everyone reading it. Treat their absence as the
contract, not as a failed request.
What a 429 looks like
Errors are RFC 9457 problem details:
HTTP/1.1 429 Too Many Requests
content-type: application/problem+json
retry-after: 37
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 37
{
"type": "https://api.developerz.ai/problems/rate-limit",
"title": "Too Many Requests",
"status": 429,
"detail": "rate limit exceeded",
"instance": "/v1/repos",
"extensions": { "retry_after": 37 }
}
Back off for retry-after seconds. The same number is repeated inside
extensions.retry_after so a client that only parses the body still has it, and
in RateLimit-Reset, because for a sliding window they are the same instant.
Two places the headers are not sent
GET /v1/openapi.jsonand the public OSS stats badge. Both are cacheable by anyone, and a per-caller counter stored in a shared cache is a wrong number served to everybody behind it until the entry expires. They still have their own per-IP limits, listed above.- Anything spelled
X-RateLimit-*. An earlier version of the API reference documentedX-RateLimit-*headers and a 1000-per-minute per-account limit. Neither ever existed. The headers above are the real ones and use the names from the IETFRateLimitheader fields draft, with noX-prefix.
Limits you may feel that are not rate limits
Two other ceilings are easy to mistake for throttling:
- Concurrency. One paid seat admits one concurrent AI-dev runner. Work above that waits in the queue rather than being rejected. See billing.
- Spend caps. A monthly USD cap bounds the orchestration we do for you, not your model bill, and pauses dispatch when it is reached. Your inference spend is billed by your own provider on your own key; cap it in their console. See billing.
Both are described on the billing page. Neither produces a 429.