Docs
Rate limits
Five per-tenant budgets, a per-IP budget on the public endpoints, and a handful of per-operator buckets. All of them scale with the tenant's licence tier.
Rate limiting on the X-Radius API is not one number. It is a small set of fixed one-minute windows, each bounding a different cost, and a request can be counted by more than one of them at once.
Every limit below is a per-minute counter in a fixed window, shared across API replicas through Redis so the budget is the whole instance's, not one process's.
What a 429 looks like
{
"error": {
"code": "ERR_RATE_LIMITED",
"message": "Too many requests. Please slow down and try again shortly.",
"request_id": "c41e77b920",
"details": { "reason": "rate_limited" }
}
}
There is no Retry-After header and no header reporting remaining budget.
The window is one minute, so a client that waits a minute is always inside a
fresh window; backing off exponentially from a few seconds is fine and kinder.
The details.reason is always the literal rate_limited. It does not name
which bucket you exhausted — infer that from the endpoint you called.
The five per-tenant budgets
These bound an authenticated tenant, keyed on the tenant for a manager credential. They are the ones an integration meets.
| Bucket | Default | What it counts | Environment knob |
|---|---|---|---|
t_mutate |
300/min | Every non-GET on the authenticated surface. Mounted once for the whole group, so no write endpoint is unbounded. | RL_TENANT_MUTATE |
t_values |
10/s | The dashboard value poll (GET /admin/dashboards/me/values and the tile refreshes). |
RL_TENANT_VALUES |
t_heavy |
30/min | Expensive reads and renders: reports, export and import enqueues, PDF and design renders, backups, bulk apply. | RL_TENANT_HEAVY |
t_probe |
10/min | Operator-triggered network probes at third-party targets: NAS ping, NAS and SSH tests, SNMP interface walks, console mints. | RL_TENANT_PROBE |
t_egress |
5/min | One request that fans out to many outbound messages: announcements, Telegram broadcast, webhook test and replay, notification-rule bulk create. | RL_TENANT_EGRESS |
t_mutate passes GET, HEAD and OPTIONS straight through, so reads cost it
nothing. The other four are stacked on individual routes next to their
permission check, which means a single heavy write is counted by t_mutate
and by t_heavy. Budget accordingly: 30 report requests per minute will
also have consumed 30 of nothing, since reports are GETs, but 30 export
enqueues consume 30 of t_heavy and 30 of t_mutate.
t_values is counted in the same fixed one-minute window as the rest, so its
ceiling per window is ten times sixty, or 600. It exists because t_heavy did
not fit it: heavy is 30 per minute for the whole tenant and was sized for a
handful of administrators opening report pages, while the dashboard values
route is polled on a timer by every signed-in manager. A twenty-seat tenant
would exhaust heavy in a single poll cycle.
Budgets scale with the licence tier
The numbers above are the reference. What a tenant actually gets is the reference scaled by its licence resource-share tier:
| Tier | Share of the reference | t_heavy in practice |
|---|---|---|
| small | 25% | 8 |
| medium | 50% | 15 |
| large | 100% | 30 |
| xl | 200% | 60 |
| max | 400% | 120 |
Every one of the five buckets scales by that same share, so multiply any
reference above by it. A small-tier tenant gets 75 writes a minute against the
300 t_mutate reference, and 2 egress calls against 5.
Scaling rounds up, never to zero — a 5-per-minute reference at 25% is 2, not 1.
The tier is derived from the licensed subscriber cap unless an operator pins it:
up to 100 subscribers is small, up to 1,000 medium, up to 10,000 large, and
anything above that or an uncapped licence is xl. The max tier is
explicit-only.
The split is deliberate and one-directional: the operator sets the reference, the licence sets the slice of it, and a tenant can set neither. There is no API call that raises your own ceiling.
One exception is worth knowing: a subscriber-portal credential is bucketed
per subscriber, not per tenant, and is not tier-weighted. A tenant-wide portal
ceiling would let one busy evening of voucher recharges return 429 to the
entire subscriber population, so one abusive subscriber is throttled instead.
Per-operator budgets on sensitive endpoints
A handful of authenticated endpoints carry their own bucket keyed on (tenant, manager) rather than on the tenant. They exist because the endpoint is an oracle as much as an operation — a code-guessing surface, an id-space probe, or a credential mint — and a per-tenant budget would let one operator spend the whole site's.
| Bucket | Default | Endpoints | Knob |
|---|---|---|---|
user_list |
240/min | GET /users |
RL_USER_LIST_PER_MIN |
user_resolve |
120/min | GET /users/resolve |
RL_USER_RESOLVE_PER_MIN |
card_search |
120/min | GET /cards/search |
RL_CARD_SEARCH_PER_MIN |
card_timeline |
120/min | GET /cards/{id}/timeline |
RL_CARD_TIMELINE_PER_MIN |
card_redeem |
60/min | POST /cards/redeem, /admin/cards/redeem-otc, /cards/redeem-create |
RL_CARD_REDEEM_PER_MIN |
card_wallet_redeem |
60/min | POST /admin/cards/redeem-to-wallet |
RL_CARD_WALLET_REDEEM_PER_MIN |
user_login_as |
30/min | POST /users/{id}/login-as |
RL_USER_LOGIN_AS_PER_MIN |
account_reauth |
10/min | POST /auth/account/password, /2fa/disable, /2fa/recovery-codes |
RL_ACCOUNT_REAUTH_PER_MIN |
api_token_reveal |
10/min | POST /developer/api-tokens/{id}/reveal |
RL_API_TOKEN_REVEAL_PER_MIN |
These are flat — the licence tier does not scale them, because they bound a security property rather than a resource share.
Note that card_timeline counts a denied lookup too. The limiter is mounted
ahead of the visibility check on purpose: behind it, a probe of the id space
would have been completely unmetered, which is the exact thing the bucket
exists to stop.
The public endpoints
Unauthenticated routes are throttled per source IP, at 20 per minute by
default (PUBLIC_RATE_LIMIT / PUBLIC_RATE_WINDOW), with each endpoint on its
own named bucket so login and tenant lookup do not share a budget. Login,
self-service registration, host resolution, slug availability, captcha, payment
returns and the public site forms are all in this class.
The zero-login portal exchange endpoints get a separate, higher budget —
120 per minute (PORTAL_ZEROLOGIN_RATE_LIMIT) — because they are called
through a relay or from behind site NAT, where every legitimate subscriber
shares one source IP.
Writing a client that does not trip
- Spread work over the window. The counter is a fixed window, not a token bucket, so 300 writes in the first second and 300 in the next are two full windows back to back. A steady rate is strictly better than bursts.
- Do not page for bulk data. A loop over a large table burns
t_mutatebudget on nothing useful and will hit the offset cap anyway. Enqueue an export job instead. - Batch the bulk endpoints. Most collections have a bulk verb that takes an
array of ids. One
POST /users/bulk-disablewith 500 ids costs one request againstt_mutate; five hundred individual calls cost five hundred and will not fit in a minute. - On
429, back off and retry the same bytes. If the operation carries arequest_id, reuse it — see idempotency. A429is the one error class where retrying identical bytes is the correct response. - Treat probe and egress endpoints as manual actions. At 10 and 5 per minute for a standard licence, they are sized for an operator clicking a button, not for a polling loop.
If your integration genuinely needs more than the tier provides, the ceiling is raised on the licence by the operator of the instance, not through the API.
Last updated