Rate limits
Limits are per credential and per workspace, enforced identically over REST and MCP. Every authenticated response tells you where you stand, not just the ones that refuse.
The limits
Two buckets are charged on a metered call: the one belonging to your credential, and the one belonging to your whole workspace. Whichever empties first is the one that refuses you.
| Bucket | Limit | Burst capacity |
|---|---|---|
send, per credential | 100 per minute | 200 |
get_pending, per credential | 60 per minute | 120 |
validate, per credential | 50 per minute | 100 |
protocol, per credential | 300 per minute | 600 |
| Workspace aggregate | 1000 per minute | 2000 |
protocol is the catch-all: every authenticated call that is not one of the three named above is charged against
it. That is the whole session-state surface — build sessions, the decision log, backlog, lessons, rules, usage —
across both REST and MCP. A call is charged to exactly one credential bucket, never two: the named category if it
has one, protocol otherwise.
Three hundred a minute is deliberately far above human-paced agent work — opening a build session is five or six calls — so a well-behaved client never meets it. It exists to stop a runaway loop, not to pace you.
This does not narrow what the pricing page calls unlimited. The protocol is unmetered in the sense that matters: no monthly quota, no per-call charge, nothing counted against your plan. Unlimited is not the same as infinitely fast.
Buckets are token buckets, not fixed windows. They refill continuously — a per-minute limit of 100 accrues at roughly 1.67 tokens a second — and hold twice the per-minute limit, so a burst of double the rate is admitted and then paid back. A client running steadily under the limit never sees a refusal; a client that arrives in spikes gets one burst free and is then paced.
The same limits apply to MCP tool calls as to the equivalent REST routes. Moving between interfaces does not buy throughput — the bucket belongs to the credential, not to the transport.
The headers
Every authenticated response carries the current state of both buckets, whatever its status code. You do not have to be refused to find out how much room you have:
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Credential | The per-minute limit of your credential’s bucket |
X-RateLimit-Remaining-Credential | Tokens left in it |
X-RateLimit-Limit-Workspace | The per-minute limit of the workspace aggregate |
X-RateLimit-Remaining-Workspace | Tokens left in it |
X-RateLimit-Reset | When the credential bucket next accrues a token, as a UTC timestamp |
On a metered call these are the values actually charged — and since protocol covers everything else, an
authenticated call is nearly always reporting a bucket it really was charged. On a response that was not charged —
an idempotent replay, or a call refused before it reached the limiter — they are read without charging, so finding
out where you stand never costs you a token.
When you are refused
A refusal is 429 with POSTMQ_RATE_LIMITED and a Retry-After header in whole seconds. The body says which
bucket ran out:
{
"error": {
"code": "POSTMQ_RATE_LIMITED",
"message": "Rate limit exceeded.",
"details": {
"bucket": "credential",
"retry_after_seconds": 3,
"limit": 100,
"window": "60s"
}
},
"request_id": "01K…"
}
bucket is the part worth reading. credential means this caller is going too fast and slowing down will fix
it. workspace means the whole workspace is at its ceiling — slowing this client down may not help if another one
is responsible, so the fix is usually to find the other client.
Per-message throttles
Two operations are additionally throttled per message, independently of the buckets above. Both exist to stop a consumer thrashing a single message:
| Operation | Limit | Code |
|---|---|---|
extend_lease | 1 per 10 seconds, per credential and message | POSTMQ_EXTEND_RATE_LIMITED |
nack | 1 per 30 seconds, per credential and message | POSTMQ_NACK_RATE_LIMITED |
Neither undoes the operation you already did: when an extend_lease is throttled your previous extension is still
in force, and when a nack is throttled the redelivery time the previous one set still stands. Honour
Retry-After and carry on.
Authentication failures
A source IP producing more than 60 authentication failures in any 60-second window is refused for five minutes
with 429 POSTMQ_RATE_LIMITED.
Only failures count. A successful call contributes nothing however busy the caller, and neither does a
403 POSTMQ_FROZEN — that credential is valid, and its refusal is an authorisation verdict rather than an
authentication failure. A missing Authorization header on an authenticated route does count.
In practice this bites during setup, when a credential has been mistyped, and almost never afterwards.
Backing off well
- Honour
Retry-After. It is computed from the bucket’s actual refill, so it is the shortest correct wait. Retrying sooner cannot succeed and only deepens the hole. - Add jitter. If ten workers are refused at once and all retry after exactly three seconds, they collide again. Spread retries over a random interval around the advertised one.
- Watch
X-RateLimit-Remaining-Credential, not just the 429s. Slowing down at 10% remaining is much cheaper than being refused and recovering. - Long-poll instead of spinning.
get_pendingtakes awaitof up to 20 seconds. One long-polled call every 20 seconds costs 3 tokens a minute; polling every second costs 60 and finds the same messages. validatehas its own budget. Checking generated payloads before sending is cheap insurance and does not spend thesendbucket.