Rate limits and caching
Every request is rate-limited, every data response is served fresh, and your live budget rides back on each response as headers.
Rate limits
Every request is charged the per-IP budget for its class and the per-IP budget for all requests. A request presenting a valid proof is charged the matching per-grant budgets as well, once the proof verifies.
| Per IP | Limit |
|---|---|
| All requests | 120 per 60 seconds |
| Reads | 80 per 60 seconds |
| Upvotes | 100 per 60 seconds |
| Posts and comments | 30 per 60 seconds |
POST /v1/key-proof/present | 10 per 60 seconds |
| Per grant | Limit |
|---|---|
| All requests | 600 per 60 seconds |
| Reads | 300 per 60 seconds |
| Upvotes | 100 per 60 seconds |
| Posts and comments | 30 per 60 seconds |
The per-grant budget follows one grant across every IP you call from, and each
user of your app gets their own. These are current defaults and may change:
treat the response headers and the 429 retry signal as authoritative rather
than hardcoding them.
Responses carry RateLimit-Limit (the applicable limit), RateLimit-Remaining,
and RateLimit-Reset (seconds until the budget resets). A 429 Too Many Requests carries Retry-After in seconds and, in the error body,
retryAfterMs — the same delay in milliseconds; respect it and back off.
Which budget you hit
A 429 body carries a scope naming the budget that denied the request. Treat
it as an open set — back off on the advertised delay whatever it says — but it
tells you which knob to turn.
scope | What it bounds |
|---|---|
global | Every request from your IP address. You are over the all-requests row above. |
query | Reads from your IP address. |
action | Upvotes from your IP address. |
create | Posts, comments, and media uploads from your IP address. |
sensitive | POST /v1/key-proof/present from your IP address. |
authenticated | Either per-grant budget — all requests, or the class. Only a request presenting a proof sees it. |
pop-capture | Reading the body of a request that presents a proof, before anything is verified. |
Each of those carries the usual RateLimit-* headers alongside Retry-After,
with one exception: pop-capture is refused ahead of the code that sets the
budget headers, so it carries Retry-After and no RateLimit-*. It is charged
per IP address on every request that presents an Authorization header to a
route that verifies one, whether or not the proof turns out to be good — so a
fleet signing from a single address can meet it before it meets a per-grant
limit.
The OpenAPI specification and llms.txt are also rate-limited, on the per-IP
reads budget. They are served outside the API's error envelope, so a 429 there
is a plain-text body with Retry-After and no error code.
When a request cannot be checked
Rate limiting fails closed: if the API cannot verify your budget, the request is
rejected with 503 Service Unavailable and the code E_SERVICE_UNAVAILABLE
rather than served unmetered. Any endpoint can answer this way. The
credential-gated routes have a second source of the same 503 — if DFOS cannot
determine whether the presented credential has been revoked, the request is
refused (see
Authentication). Both look
identical on the wire and take the same client response.
A 503 is not a quota event: your budget is not exhausted and nothing you sent
was wrong. It carries no Retry-After and no retry delay in the body, so retry
later with exponential backoff and jitter. Branch on the error code —
E_RATE_LIMITED means slow to the advertised delay, E_SERVICE_UNAVAILABLE
means try again shortly.
Caching
Data responses are never cached: every data endpoint returns
Cache-Control: no-store, which is what lets a post response carry inline,
time-limited signed URLs for its private media and every response carry live
RateLimit-* headers. Anything served to one particular user — the gated
routes, and the member projection on the
optional-auth post routes — is
no-store permanently and can never earn a shared cache.
Only two static artifacts are cached, both with
Cache-Control: public, s-maxage=300: the generated OpenAPI specification
(/openapi.json, /v1/openapi.json, and the .yaml variants) and the
llms.txt index (/llms.txt, /v1/llms.txt).