Skip to main content

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 IPLimit
All requests120 per 60 seconds
Reads80 per 60 seconds
Upvotes100 per 60 seconds
Posts and comments30 per 60 seconds
POST /v1/key-proof/present10 per 60 seconds
Per grantLimit
All requests600 per 60 seconds
Reads300 per 60 seconds
Upvotes100 per 60 seconds
Posts and comments30 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.

scopeWhat it bounds
globalEvery request from your IP address. You are over the all-requests row above.
queryReads from your IP address.
actionUpvotes from your IP address.
createPosts, comments, and media uploads from your IP address.
sensitivePOST /v1/key-proof/present from your IP address.
authenticatedEither per-grant budget — all requests, or the class. Only a request presenting a proof sees it.
pop-captureReading 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 codeE_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).