Every surface is metered, and every request — including the rejected ones — is logged.
Authenticated content keys
Two rolling windows, and both must have budget or the request is rejected:
| Window | Setting | Default |
|---|---|---|
| 5 hours | api.rate.5h |
500 requests |
| 7 days | api.rate.week |
5000 requests |
Both are per role: an operator sets one base figure for everybody and overrides the roles that get more. That replaced a pair of environment variables per window — one for members and one for supporters — which hard-wired the tiers to “supporter or not” and could not give a moderator its own number.
The quota is counted against the user, not the individual key, so a second key does not buy a second budget.
Live usage for both windows is shown under Account → API keys, alongside an audit log carrying every request’s method, path, status, address and the item it touched.
List caching and limits
| Setting | Default | |
|---|---|---|
api.list.limitDefault |
25 | The page size when you ask for none |
api.list.limitMax |
100 | The ceiling. Per role |
api.list.cacheSec |
30s | How long an identical list response is served from cache |
A limit above the ceiling is clamped down rather than refused, so a caller
asking for a thousand gets a page and a pagination block that says what it
actually got.
Anonymous reads
Per source address, in two windows, both of which must have budget:
| Setting | Default | Window |
|---|---|---|
api.anon.rateMin |
30 | per minute |
api.anon.rateDay |
2000 | per day |
The numbers are small on purpose. One address is a household, an office or a carrier NAT, so a generous per-IP budget is a generous budget for a botnet with one node behind each address.
The answer to wanting more is a key — which is free, attributable and revocable, as an address is not.
Headers
Every response carries x-ratelimit-limit, x-ratelimit-remaining and
x-ratelimit-reset (plus -day variants) and x-ratelimit-backend. A 429
carries retry-after. Answers are cached for 60 seconds by default and carry
x-cache: HIT|MISS.
It degrades rather than refusing
The rule is that an anonymous request which cannot be counted is an unbounded public endpoint. But “cannot be counted in the cache” is not “cannot be counted”:
- No cache available
The quota is counted in each app process’s own memory (
x-ratelimit-backend: memory). The bound survives; the exactness does not — a deployment running N processes then allows up to N times the budget, and the day window resets on deploy. Requests past it still cost no database query.- No readable source address
Every such request counts against one shared bucket, and the
429says so. This is a proxy misconfiguration, and it is logged once a minute rather than per request.
A correction, if you integrated against the old behaviour
Both cases above used to be answered 429 rate_limited with “Unauthenticated
requests are temporarily unavailable”. Since the cache is optional and every
other consumer treats a missing cache as a cache miss, a deployment that never
configured one looked entirely healthy while refusing 100% of anonymous traffic,
permanently — under a status code that sent integrators to look at their own
call frequency.
A 429 now only ever means a 429.
Integrations
Two budgets, both enforced, both operator-configurable:
| Budget | Scope | Setting |
|---|---|---|
| Per credential | This integration | integration.rate.max |
| Per source address | Every integration reporting from one host | integration.rate.perIp |
You choose your own per-credential figure at creation (600/minute by default), and it is clamped to the operator’s current ceiling at request time — so lowering the ceiling reins in credentials created under a higher one rather than grandfathering them.
The per-address budget is what bounds a leaked-token farm: somebody holding fifty credentials still only gets one address’s worth of budget.
Over either limit is a 429 for the rest of the minute. Back off; do not retry
immediately.
There is also a cap on how many integrations may exist against one content item at a time (10 by default). Revoked ones do not count.
Row budgets are separate
The limits above bound how often you may report. They say nothing about how much each report leaves behind, and for endpoints that append rather than overwrite that is the part that matters — a reporter inside its request budget can still write millions of rows a day.
Those ceilings, and the fact that every one of them degrades rather than refusing, are documented on Reporting server statistics and Integration API — parties.
The app API
Rate limits are per endpoint. The two on the device-login flow are durable — counted in the database rather than only in the cache — because that is the front door, and a cache outage must not turn it into an unmetered row factory.
| Endpoint | Limit |
|---|---|
POST /auth/device |
10 per minute |
POST /auth/token |
60 per minute — a well-behaved client polls about 12 times a minute |
POST /auth/handoff |
20 per minute, also durable — it mints credentials |
GET /apps |
120 per minute |
POST /stats/submit |
30 per minute |
GET /stats/me, GET /stats/top |
60 per minute |
A token is what a limit is counted against wherever there is one — which is why
/apps accepts a bearer despite needing none: the alternative is charging a
whole household behind one address as though it were one caller.