Skip to main content
ModuleX rate-limits the REST API at several independent layers. Most callers only meet the per-key and per-user request limiters, which return a 429 with a plain string detail and a standard set of rate-limit headers. Workflow runs and other managed-usage surfaces add a run-rate limiter that returns the flat DenialEnvelope, and organization-scoped routes add a third org api-class limiter. This page documents every limiter, every 429 wire shape, the headers each carries, and how to back off. This page covers the HTTP API only. The realtime Socket.io collaboration server enforces a separate transport-level flood limit that is not an HTTP 429 — see its own section below and /realtime/socket-events.

The rate-limit layers at a glance

All limiters use a sliding 60-second window. The per-key and per-user limiters are checked together — a request is admitted only if both have remaining capacity, and the layer that ran out determines the detail string.
A single 429 status can carry three different bodies depending on which limiter fired. The per-key/per-user limiter returns a string detail; the org limiter returns a dict detail with code: "rate_limited"; the run-rate limiter returns a flat DenialEnvelope with code: "rate_limit_exceeded". Branch on the response body, not just the status code. The full taxonomy lives on /api-reference/errors.

Per-key and per-user limits

Every request authenticated with an mx_live_… API key passes through a two-level request limiter before it reaches a route handler. Both levels share the same sliding 60-second window.
  • Per-key — each API key has its own rate_limit_per_minute. The default is 60 requests per minute. The rate-limit store counter key is ratelimit:apikey:{api_key_id}.
  • Per-user — across all of a user’s API keys, the aggregate limit is 300 requests per minute. The rate-limit store counter key is ratelimit:user:{user_id}.
A request is admitted only when both counters are below their limit. When the per-key limit is hit first, the detail is "API key rate limit exceeded"; when the per-user aggregate is hit first, it is "User rate limit exceeded (across all API keys)". Both responses are HTTP 429 with the same header set.
integer
default:"60"
The per-key request ceiling, stored on each API key. New keys are created with 60; an administrator can raise or lower it per key. The per-user aggregate of 300 req/min applies on top and is not configurable per request.
This limiter authenticates and counts the request before any route runs, so it applies uniformly to reads, writes, and run triggers made with an API key.

429 response — per-key / per-user (Shape A)

429 Too Many Requests — Shape A
The response carries these headers:
integer
The ceiling for the limiter that produced the response (the per-key limit, e.g. 60).
integer
Requests left in the current 60-second window. 0 when the limit is hit.
integer
Unix epoch time in seconds when the window resets and capacity is restored.
integer
Seconds to wait before retrying. Always present on a 429; defaults to 60 when a more precise value is not available.

Org api-class limit

Org-scoped routes (those that require organization membership via X-Organization-ID) add an organization api-class limiter on top of the per-key and per-user limiters. Its ceiling comes from your plan’s api rate-limit entitlement, so it is plan-dependent rather than a fixed default. See /billing/usage-gating and /billing/plans for the per-plan values. This limiter stacks with the others — an org-scoped API-key request is counted against the org api-class counter, the per-user counter (300/min), and the per-key counter. It fails open on any error, so a rate-limit store outage never blocks org traffic. Administrative routes and scheduled runs do not pass through it.

429 response — org api-class (Shape B)

The org limiter returns the structured envelope wrapped under detail (a dict-valued detail), with code: "rate_limited":
429 Too Many Requests — Shape B
This response carries the same X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and Retry-After headers documented above.

Run-rate limits (run / managed-usage surfaces)

Workflow runs and the other managed-usage surfaces — the AI Composer, the Assistant, and managed knowledge retrieval and ingestion — pass through an additional run-rate limiter as part of the billing admission gate. The schema defines two plan-scoped classes, but only one is live today:
  • sync_exec — synchronous (blocking) run executions per minute. This is the only run-rate class that is actually consumed: every run and managed-usage surface counts against sync_exec, so it is the only one that can produce a live 429.
  • async_exec — defined in the entitlement schema for asynchronous run executions and reserved for a future surface. It is not an active limit today; treat the synchronous sync_exec class as the live run-rate limit.
The sync_exec ceiling comes from your plan’s rate-limit entitlements, so the exact number is plan-dependent. A null entitlement means unlimited (the limiter is skipped), and a 0 entitlement means blocked. See /billing/usage-gating for the per-plan run-rate values and /billing/plans for the plan matrix. When the run-rate limiter denies a request, it first releases any credit reservation the gate had taken (so a blocked run is charged nothing), then raises a 429 carrying the flat DenialEnvelope. This is the form you must handle on these surfaces alongside the header-based 429.
The run-rate limiter is part of the live billing admission gate. It applies on the run / Composer / Assistant / managed-knowledge surfaces and is absent on plain CRUD and org-settings routes — those return only the {"detail": …} shapes. The same gate also produces 402 (credit/wallet) and 403 (quota) denials; those are covered on /api-reference/errors and /billing/usage-gating.

429 response — run-rate (Shape D, DenialEnvelope)

The run-rate denial is a flat object with no detail wrapper and code: "rate_limit_exceeded":
429 Too Many Requests — Shape D
A rate-layer DenialEnvelope also carries the rate-limit headers, each omitted when its underlying value is unknown rather than sent as an empty string. Retry-After is always present and defaults to 60.
integer
Seconds to wait before retrying. Defaults to 60.
integer
The class ceiling. Omitted when unknown.
integer
Runs left in the window. Omitted when unknown.
integer
Unix epoch (seconds) when the window resets. Omitted when unknown.

Reading the headers

Every 429 response — whichever limiter fired — carries Retry-After, and where the value is known, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. The example below triggers a workflow run, then inspects the headers on a 429. Replace mx_live_… with your API key and the IDs with your own.
The two SDKs surface 429s differently. The JavaScript SDK maps every 429 to RateLimitError regardless of body shape. The Python SDK maps only the string-detail header 429 (Shape A) to RateLimitError. Both the FastAPI-wrapped dict-detail envelope (Shape B) and the flat rate-layer DenialEnvelope (Shape D) raise the base BillingError with layer == "rate" — because the Python SDK’s layer-to-subclass map has no "rate" key, so any rate-layer envelope (wrapped under detail or top-level) falls through to base BillingError. Catch both RateLimitError and BillingError in Python to handle every 429. See /sdks/errors-retries.

Backing off and retrying

Both SDKs treat 429, 500, 502, and 503 as retryable and honor Retry-After when deciding how long to wait between attempts; 400, 401, 403, 404, 409, and 422 are not retried. The Python client retries GET and HEAD requests only, while the JavaScript client retries all methods. SSE stream connection errors are surfaced once and are not auto-reconnected. When you back off by hand, prefer Retry-After over a fixed sleep, and add jitter so concurrent clients do not retry in lockstep:
The per-user aggregate of 300 requests per minute counts every key a user owns, so adding more API keys for one user does not raise the ceiling. For high-volume runs, watch the live run-rate sync_exec class on /billing/usage-gating (async_exec is reserved for a future surface) and consider a plan with higher run ceilings on /billing/plans.

Socket.io transport rate limit (not a 429)

The realtime collaboration server enforces its own per-user flood limit on socket messages. This is a transport-level guard and is deliberately distinct from the HTTP rate limits above — it is not an HTTP 429 and it does not carry a DenialEnvelope. When a connected user sends gated events too quickly, the server emits an error event:
Socket.io error event
The retryAfterMs hint is included only on a genuine limit hit; a store error blocks the operation without it (the limiter fails closed here). The limiter is keyed per user across all gated events. For the full Socket.io event reference, see /realtime/socket-events.
Do not confuse the Socket.io code: "rate_limited" (transport, with message and retryAfterMs) with the HTTP org-limiter code: "rate_limited" (a 429 with a dict detail) or the run-rate code: "rate_limit_exceeded" (a 429 with a flat DenialEnvelope). They are three separate mechanisms on two different transports.

Errors & status codes

The three HTTP error-envelope shapes and which surface emits each, including the full status taxonomy.

Usage gating & limits

The billing admission gate and the per-plan run-rate ceilings that drive Shape D 429s.

Authentication

Authenticate every request with Authorization: Bearer mx_live_… and X-Organization-ID.

SDK errors & retries

SDK error classes, the retry policy, and how each SDK surfaces a 429.