Rate limits
Rate limits describe request limits for API use.
Rate limits protect Dreamstate API and MCP access from request bursts. Treat a 429 response as a request-level refusal, not as an empty catalog result, a missing provider record, or a completed capability run.
The active limit depends on the authenticated identity and on the capability family. Discovery, ordinary API work, externally effective work, and credential or root work can therefore receive different refusal codes even when they originate from the same workspace.
Limit families and their scopes
The API-key middleware applies a per-key limiter, a workspace limiter, and additional family limiters for external-effect and credential or root capabilities. The pre-auth middleware runs before API-key lookup. It keys verified signed-in sessions by user id and other traffic by client IP.
All listed API and pre-auth limits use a 60,000 millisecond window. The table gives the actual families exported by the middleware, not a suggested client configuration. A client should read the returned refusal rather than assume that its next request used the ordinary per-key bucket.
| Family | Limit | Keyed by | Refusal code | Why this bucket exists |
|---|---|---|---|---|
| Per API key | 100 requests per minute | One authenticated API key | rate_limited | Prevents a single key from exhausting the standard request budget. |
| Per workspace | 120 requests per minute | All authenticated keys in one workspace | workspace_rate_limited | Key rotation does not multiply a workspace budget. |
| External effect | 20 requests per minute | A workspace calling a capability classified as external effect | external_effect_rate_limited | Calls that create an external effect use a lower family budget. |
| Credential or root | 10 requests per minute | A workspace calling a credential or root capability | credential_root_rate_limited | Credential and root capability requests use the lowest family budget. |
| Pre-auth IP | 300 requests per minute | Anonymous, invalid, or API-key-shaped traffic from one client IP | ip_rate_limited | The pre-auth path limits traffic before API-key lookup. |
| Pre-auth session | 2400 requests per minute | A verified signed-in session user | session_rate_limited | A verified session is keyed by user id instead of a shared network address. |
Two API keys in one workspace share the workspace budget. The middleware test sends seventy ordinary requests with one key and fifty with another, then verifies that the next workspace request receives workspace_rate_limited. A second key is not a way to continue after a workspace refusal.
An external-effect capability can be refused before the ordinary per-key or workspace ceiling. The middleware identifies external-effect ids from catalog effect classification, then applies the workspace family bucket. Credential and root capability ids use a separate family bucket with their own code.
Read the 429 response
Every refusal from this middleware returns HTTP 429 and sets Retry-After. The JSON body carries the stable code, a readable message, the same retry interval in seconds, the window duration, and the limit that applied. Persist the code with your request telemetry so a client can distinguish a key issue from a workspace or family burst.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{
"error": {
"code": "rate_limited",
"message": "100 requests per minute per API key exceeded",
"retry_after_seconds": 60,
"window_ms": 60000,
"limit": 100
}
}| Response field | Type | When present | Meaning | Default |
|---|---|---|---|---|
HTTP status | number | Always on refusal | 429 identifies a rate-limit refusal. | Not applicable |
Retry-After | HTTP response header, integer string | Always on refusal | The number of seconds before a retry can be attempted. | Not applicable |
error.code | string | Always on refusal | The stable code for the limiter that refused the request. | Not applicable |
error.message | string | Always on refusal | A readable explanation of the exceeded family budget. | Not applicable |
error.retry_after_seconds | number | Always on refusal | The retry interval represented in the JSON response. | Not applicable |
error.window_ms | number | Always on refusal | The window duration used by the limiter. | 60000 |
error.limit | number | Always on refusal | The request count allowed for the family in that window. | Family-specific |
The retry interval is calculated from the limiter reset time when one exists, otherwise from the configured window. It is at least one second. In the API-key middleware test, a per-key refusal returns a 60 second JSON interval and a Retry-After: 60 response header.
Use the fields in the response instead of deriving an interval from local clock assumptions. The server may be shared across instances, and the middleware is designed so that its enforced budget is not only a local process counter.
The response limit names the bucket that stopped the request. Keep that number with the error code when you report an integration issue. A later request may reach a different bucket, so an earlier ordinary-read refusal does not prove that an external-effect call will receive the same code.
Retry without changing the request
Wait for the returned retry interval before scheduling another request. Preserve the same capability parameters, mode, quote, turn token, and idempotency identity when the operation itself is a retry. Rate limiting changes when a request can be retried. It does not change the fetched contract for the capability.
If your client owns a queue, pause only the work that shares the refused bucket. A per-key refusal concerns that API key. A workspace or family refusal can affect other workers in the same workspace, so continuing those workers immediately can create more 429 responses.
The example intentionally retries the same request after the server-provided interval. For a live mutation, pair this behavior with Idempotency so a request that was accepted before the network response is not treated as a new operation.
Understand the classification
A reply through unibox.context.thread_reply_send is classified as an external effect; tables.contacts.list is standard. The middleware classifies workspace.api_keys.create and workspace.account.delete as credential or root work. These examples explain the family model, not a substitute for discovering the current capability contract before a run.
REST run paths expose the capability id in the path. MCP requests expose it inside a tools_run call body. The limiter extracts that id to classify the request family, which keeps the same family logic available through both doors.
When it goes wrong
A 429 is recoverable after the stated interval. The correct repair is to use the returned code and retry data. Do not fall back to a guessed capability id, do not silently switch the workspace, and do not treat the refusal as a completed run.
If the code is rate_limited, reduce or queue requests made by the relevant API key. If it is workspace_rate_limited, coordinate work across keys in that workspace. If it is external_effect_rate_limited or credential_root_rate_limited, pause the respective family rather than ordinary read work.
If an unauthenticated or invalid credential request receives ip_rate_limited, wait before retrying from that client IP. A verified signed-in session uses session_rate_limited instead, because the pre-auth middleware can key the request by its verified session user. Authentication errors and capability input errors have their own responses, so retain the exact code.
After the interval, send one controlled retry and inspect its response. The test advances the clock beyond one window and verifies that the per-key request succeeds again. That is the client behavior to model: wait, retry the same intended operation, then decide from the new envelope.