Appearance
Rate limits
Every request to reportd counts against a rate limit for its class: sign-ins, signed-out requests, reads, writes, renders and the assistant. The studio and normal integrations never meet the defaults; a runaway script or agent does. A refused request gets 429 Too Many Requests with Retry-After.
The limits are on top of the sign-in throttle (5 failed sign-ins for a username, or 20 from one address, in 15 minutes), the MCP tool-call limit (mcp.rateLimitPerMinute) and the agents' edit limit (agents.maxOpsPerMinute). They are rates, not quotas: nothing counts renders or pages per day or month.
Classes
| Class | Requests | Counted per | Default per minute | Burst |
|---|---|---|---|---|
signin | POST /v1/login, /v1/login/mfa…, everything under /v1/auth/ | client address | 60 | 20 |
public | any other request without a valid token, such as GET /l/{token}, or one with a missing or bad token | client address | 300 | 100 |
read | GET and HEAD, signed in | user or key | 1200 | 300 |
write | POST, PUT, PATCH and DELETE, signed in, outside the two classes below | user or key | 900 | 120 |
render | POST /v1/render, GET /v1/view/sessions/{id}/render, POST /v1/templates/{id}/validate, …/data, …/tests/run, POST /v1/jobs, POST /v1/sources/{name}/describe | user or key | 120 | 30 |
assistant | POST /v1/templates/{id}/assistant | user or key | 30 | 10 |
- Not limited:
/health,/livezand/readyz, and the studio's and viewer's own files. - Each class is a token bucket. It holds up to its burst and refills at its rate per minute, so a client can send a burst at once and then the rate.
- A user has one bucket per class for all their sessions and tabs together. An API key or agent key has its own, so an agent doesn't use up the budget of the user it acts for.
- Signed out, and with a bad token, a request counts by the client's address (an IPv6 address by its /64). Guessing tokens can't use up a real user's budget.
- Work reportd does itself isn't counted: a batch job's runs, and the routes MCP tools and the assistant's tools call inside one request.
Config
yaml
limits:
rate: # per minute and burst; perMinute 0 turns a class off
signin: {perMinute: 60, burst: 20}
public: {perMinute: 300, burst: 100}
read: {perMinute: 1200, burst: 300}
write: {perMinute: 900, burst: 120}
render: {perMinute: 120, burst: 30}
assistant: {perMinute: 30, burst: 10}- A class left out keeps its default, and so does a burst left out.
limits.rate: {}keeps every default. perMinute: 0turns a class off.reportd init-configwrites the block with the defaults, for a server and for a workstation alike.- reportd refuses to start with a class it doesn't know, or a negative number.
On state.driver: file the limits live in each reportd's memory, as the sign-in throttle does: a restart forgets them, and with several reportd behind a load balancer, each counts its own requests. On postgres (state.md) every node counts in one bucket per class and key, one statement per request; while Postgres can't be reached, each node counts in its own memory and logs that once a minute.
Behind a proxy
Without security.trustedProxies, the client's address is the TCP peer. Behind a reverse proxy or load balancer, that is the proxy, and every signed-out client and every sign-in shares the proxy's buckets, as they share its sign-in throttle. Tell reportd which proxies to believe:
yaml
security:
trustedProxies: [10.0.0.0/8, 127.0.0.1] # CIDRs or single addresses; empty by default
clientAddressHeader: X-Forwarded-For # or Forwarded, or X-Real-IP- Only from a trusted peer does reportd read the header. It walks its addresses from right to left, skips the trusted proxies, and takes the first that isn't one. When every address is trusted, it takes the leftmost.
- From any other peer, the header is ignored, so a client can't choose its own address.
- Only the one configured header is read. When two are read, a proxy that cleans one of them still lets a client forge the other.
- Once
trustedProxiesis set,X-Forwarded-Proto: https(which makes the session cookie Secure) counts only from a trusted peer. While it is empty, it counts from anyone, as before. With anhttpspublicURL the cookie is Secure whatever the header says. - The same address is used everywhere: the rate limits, the sign-in throttle, MFA, the OIDC and SAML callbacks, and the access log's
clientfield. - When requests carry one of the three headers and no proxy is trusted, reportd logs a warning once.
An API key's own limits
An administrator (user.manage) can give an API key or agent key its own rates, in place of the defaults, for read, write, render and assistant:
http
PATCH /v1/keys/k1a2b3c4d5e6
Content-Type: application/json
{"rateLimits": {"render": {"perMinute": 600, "burst": 60}, "read": {"perMinute": 0}}}rateLimitsreplaces the key's limits;{}goes back to the defaults.POST /v1/keysaccepts the same field.- A class left out keeps the default, and so does a burst left out.
perMinute: 0is no limit for that key and class. GET /v1/keysshows each key's limits. Each change is audited askey.rate-limits <id>.- A key's limits apply to that key only. An agent key's don't change the limits of the user it acts for. Users all share the defaults.
Responses
A refused request:
http
HTTP/1.1 429 Too Many Requests
Retry-After: 4
RateLimit-Policy: "render";q=120;w=60
RateLimit: "render";r=0;t=4
Content-Type: application/json
{"error": "rate-limited", "class": "render", "message": "too many requests; retry in 4s", "retryAfter": 4}Retry-Afteris in whole seconds, rounded up.- Every response a class limits carries
RateLimit-PolicyandRateLimit, the structured fields of the IETF httpapi draft draft-ietf-httpapi-ratelimit-headers-11, which may still change.qis the class's rate per window andwthe window (60 s).ris the requests left in the bucket now. Whileris above 0,tis the seconds until the bucket is full again; at 0, the seconds until the next request may pass, which on a 429 isRetry-After. - The MCP tool-call limit and the agents' edit limit answer the same 429 body, without
class. - A 429 is logged at debug level, at most once per bucket a minute.
The studio and the viewer wait out a 429 on a read, a render or a preview once, when Retry-After is at most 10 seconds. Otherwise they say "Too many requests, try again in N s". A 429 never shows the connection-lost overlay. A save the limit refused is tried again after the wait.
Memory
- Each minute a sweep drops the buckets that have refilled; a full bucket is the same as none.
- Each class keeps at most 50,000 signed-out address buckets. Past that, a new address shares one overflow bucket until the sweep frees room, and reportd logs a warning once a minute while it lasts. A flood from many addresses then limits itself, not the users who are signed in.
In front of reportd
A reverse proxy, load balancer or WAF can add limits of its own, such as per connection or per country, before requests reach reportd. reportd's limits still apply behind it, by user and key as well as by address.