When your assistant calls a juudd tool too often or sends too much at once, it does not get a 429 or a 413. It gets a sentence: what was refused, that nothing was changed, and what to do next. The same sentence is written into your account's history, beside every other time the platform said no.
That is the whole decision. The rest of this post is why a limit belongs in the same place as every other refusal, what the numbers are, and the part of it we have not finished.
An unbounded tool is a shared cost with one owner
Until 2026-09-04 nothing on the tool path bounded a caller. One valid sign-in could deploy, or create a database, as fast as it could send requests. Storage is shared between accounts, so is the place sites are served from, and so is the pool databases are created in. The cost of one looping account landed on every other account, and none of them could see why.
Nobody has to be hostile for this to matter. An assistant that misreads an error and retries in a loop does the same thing as an attacker, only more politely. A limit is not a statement about who we expect to arrive. It is a statement that one account's bad minute should stay that account's.
The limit nobody decided was the worst one
There was already one limit before we chose any, and it came from a default. The server's request body limit was 1 MiB, and a deploy carries its files inline, with anything binary as base64. So a site over roughly 750 KB of real bytes was answered 413 before our code ever saw it.
Look at what that answer was not.
- Not worded. Every refusal here says what happened and what would make the call succeed. A
413says neither. - Not recorded. Every refusal here is written down in the words the caller was given. A transport answer is written nowhere.
- Not stated anywhere. No page and no tool description said the number existed.
And the worst part was how it looked from the other side. A client shows a transport failure as the service being unreachable, and an assistant that sees an unreachable service does the reasonable thing: it retries, and then it tells you the platform is down. A limit you cannot read is indistinguishable from an outage, and we had one that nobody chose.
So a bound goes where refusals already go
Every tool that acts on a site passes through one gate, and every refusal leaves through one function in it. That gate already said not yours and this account is suspended, in words, into the record. Putting the limit there cost nothing in discipline that those did not already cost, and it bought the three things the 413 lacked.
A pace refusal reads, word for word, with the tool's name and the number of seconds filled in:
Each clause is aimed at the one reader who will act on it, which is a model. Nothing was changed stops it assuming a half-finished deploy. Wait with a number gives it a count instead of a guess. Call it once is the instruction it needs most, because the default reaction to being refused is to try again immediately.
The pace is asked first, before any record is read, so a caller over the bound costs the database nothing either. A refused call still counts, so a caller that keeps hammering does not creep back under the limit by being refused.
The numbers, and where they come from
Two kinds of bound: how often one account may call one tool, and how big one bundle may be.
| tool | calls per account per minute |
|---|---|
deploy_site, publish_deploy, request_upload | 12 |
create_database, set_database_url, claim_domain | 6 |
start_build, reset_database_credential | 3 |
apply_sql | 60 |
| every other tool | 240 |
A bundle is bounded at 4 MB as sent, text and base64 counted together, because that is the number the caller can actually see. The size is checked after the ownership check, so a stranger's oversized bundle is refused as a stranger's, and before a single byte is decoded. The transport's own limit still exists, set deliberately at 6 MiB so that a well-formed call inside the tool's bound never meets it. It is a backstop, not a rule anyone is meant to hit.
These numbers are decisions, not measurements. When they were set there was no load to measure against, so they sit where an assistant working normally never reaches them and a loop reaches them inside a minute. A person deploys a handful of times an hour. A loop deploys every few seconds. There is a lot of room between those two, and the numbers live in it.
The memory that holds the counts is the serving process's own. A restart forgets every count, and that is the failure we wanted: a limiter that came back strict after a deploy of ours would refuse the first person to arrive.
A limit belongs in the description, not only in the refusal
A refusal teaches the assistant after it has already built the wrong thing. A description teaches it before, at the only moment it is deciding how big a site to send or how often to send it. So the tools a person runs into a bound on most often say so in their own words: deploy_site states the 4 MB and the 12 a minute, request_upload states its 12, and start_build states one build at a time and 3 a minute.
Not every bound is in its tool's description yet. create_database, apply_sql and the other database and domain tools carry the limits in the table above and do not state them. They are set far enough out that an ordinary session does not meet them, which is why this was not urgent, and it is still a gap between what we decided and what the assistant can read. We would rather say that here than let a number in this post be the first place you meet it.
Why you can ask what happened on Tuesday
Because a bound is a refusal, it is kept like one. The account's list of refusals now has too fast and too big beside not yours and the schema gate stopped it, all in the same voice. list_refusals reads that list back to the assistant, so a new conversation can find out why a deploy three days ago never happened, instead of guessing or asking you to remember.
A 429 cannot do that. It is not stored, it names no tool, and it is gone the moment the client gives up on it. The limits themselves, tool by tool, are listed in the docs.
What is not decided
Three questions are open, and none of them has a customer to ask yet.
- What an account that pays should be allowed, if anything different.
- Whether the counts move from memory into the database the day more than one process serves the tools. The interface was written so that change does not move it.
- Whether you should be able to read how much of a bound you have left before you meet it.
The rule, once
A limit the caller cannot read looks exactly like an outage, so every limit here is a refusal: worded for the model that will act on it, recorded in the account's history, and, tool by tool as we close the gap, stated in advance where the assistant looks before it builds.