跳到正文

文档 / 速率限制

Rate limits

Per-token read and write budgets, the pre-auth per-IP bucket, and exactly what a 429 looks like.

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

Four sliding windows, all one minute wide. Every budget on this page is declared once, in apps/api/src/middleware/rate-limit.ts, and the constant is named next to the number so a change there is one search away from this page.

Bucket Keyed on Budget Constant
Reads Your token or session 600 / minute READ_CAPACITY
Writes, per conversation Your token or session, and the conversation 120 / minute WRITE_CAPACITY
Writes, per token Your token or session 600 / minute TOKEN_WRITE_CAPACITY
Pre-auth Caller IP 1200 / minute PREAUTH_CAPACITY

These are machine-shaped numbers on purpose. The clients of this API are agents: a box polling a run, a janitor sweeping a repo, an operator draining a stuck queue, the MCP surface you point your own agent at. All of that is legitimate, authenticated, bursty automation, which is why the read and write budgets are 600 a minute and not the one-request-a-second a human-shaped API would set.

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. It stays strictly above every per-token budget, so the fair-share bucket is always the one that binds first for a caller who has authenticated; a pre-auth number at or below the token caps would quietly become the real ceiling for everybody.

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 120 a minute (WRITE_CAPACITY), up to the token ceiling of 600 (TOKEN_WRITE_CAPACITY). Everything else shares one anonymous conversation, so its effective limit is the 120: 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 120 on one write and 600 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 the method rule alone would charge read verbs like box list to the write buckets: out of one conversation's 120 a minute (WRITE_CAPACITY) rather than the 600 a minute (READ_CAPACITY) a read is entitled to, and against the fair share the conversation needs for actual work. MCP requests are classified by the verb's scope instead: a read verb spends from the read budget.

Endpoints with their own limit

These sit outside the per-token buckets above, each with its own per-IP window and its own constant.

Endpoint Limit Constant
GET /v1/public/oss/:owner/:repo/stats 60 / minute per IP DEFAULT_RATE_PER_MINUTE (routes/public-oss.ts)
GET | POST /unsubscribe 30 / minute per IP DEFAULT_RATE_PER_MINUTE (routes/unsubscribe.ts)
GET /v1/openapi.json 30 / minute per IP OPENAPI_RATE_PER_MINUTE (routes/openapi.ts)
POST /v1/runners/enroll 60 / minute per IP ENROLL_RATE_PER_MINUTE (routes/runners.ts)
POST /v1/runners/creds/reissue 60 / minute per IP BOX_BEARER_RATE_PER_MINUTE (routes/runners.ts)
POST /v1/runners/hardening 60 / minute per IP BOX_BEARER_RATE_PER_MINUTE (routes/runners.ts)

Correction: the box lanes are capped

An earlier version of this page said POST /v1/runners/hardening was not rate-limited, and gave two reasons: that boxes behind one NAT would be refused by a per-IP cap, and that "the bearer is checked before any lookup, so it is not a flood surface either". Both halves are wrong, and the second was wrong when it was written. It is corrected here rather than deleted, because it was published.

POST /v1/runners/hardening and POST /v1/runners/creds/reissue are each capped at 60 requests a minute per IP (BOX_BEARER_RATE_PER_MINUTE, apps/api/src/routes/runners.ts), and the bucket is charged before anything else in the handler runs. The check that happens before the database lookup grades the shape of a dev_run_… bearer, not its identity, so Bearer dev_run_<junk> passes it and buys one credential-hash lookup per attempt. Uncapped, one address could loop that without bound and without a record; the cap makes each attempt a counter hit instead. The three box lanes hold separate buckets, so a flood against one cannot lock a legitimate box out of another.

The shared-egress concern is real, and it set the number rather than removing the cap. A box reports hardening once per install and rolls its credential on an hourly timer, with retries floored at one a minute, so even a crash-looping box spends about two requests a minute and a rack of them behind one NAT spends a small multiple of that. A cap of 60 leaves several times the headroom the busiest shared-egress site we have measured needs. If yours needs more, tell us and we will raise it for you.

Watching a run without spending your whole read budget

A once-a-second poll spends 60 reads a minute, a tenth of the 600 in READ_CAPACITY, on a single run. Ten watched runs and the budget is gone, and a fleet rollout (box update) runs for minutes to hours, so this is the shape of spend that grows with your fleet rather than a cap you hit once.

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, a tenth of your read budget held for the whole hour
wait_ms=25000 on an idle run about 144, well under 1% 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_seq echoes 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 404 with a problem+json body, and over MCP it is a tool result flagged isError naming 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 MCP task 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 separate bucket just for watching. We did not build one, on purpose, and raising READ_CAPACITY to 600 did not change the argument: a bigger allowance hands 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: 600
RateLimit-Remaining: 598
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 600 on a GET (READ_CAPACITY) and 120 on the POST right after it (WRITE_CAPACITY, the conversation's fair share) is correct.
  • On POST /v1/mcp the numbers follow the verb, not the method, exactly the way the bucket does. tools/list reports 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: 600
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.json and 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 documented X-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 IETF RateLimit header fields draft, with no X- 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.