Reference
Rate limits
Every limiter, its default, what it keys on, and how to tune it safely.
Rate limiting is on by default and covers logins, registrations, API writes, media uploads, the federation inbox, remote-discovery browsing, and the content webhook.
The limiters
| Limiter | Variable | Default | Window | Keyed on |
|---|---|---|---|---|
| Login | RL_LOGIN_MAX |
15 | 15 min | Source IP |
| Registration | RL_REGISTER_MAX |
5 | 1 hour | Source IP |
| API writes | RL_API_WRITE_MAX |
120 | 1 min | Signed-in user, else IP |
| Media uploads | RL_UPLOAD_MAX |
10 | 1 min | Signed-in user, else IP |
| Federation inbox | RL_INBOX_MAX |
300 | 1 min | Source IP |
| Remote discovery | RL_REMOTE_MAX |
30 | 1 min | Source IP (anonymous only) |
| Remote cache-miss | RL_REMOTE_MISS_MAX |
20 | 1 min | Signed-in user, else IP |
| Content webhook | RL_WEBHOOK_MAX |
30 | 1 min | Source IP |
Plus hard payload caps on the two endpoints that accept unauthenticated or machine-sent bodies:
| Variable | Default | Meaning |
|---|---|---|
INBOX_MAX_BODY_BYTES |
1000000 |
Maximum accepted inbox POST body, in bytes |
WEBHOOK_MAX_BODY_BYTES |
512000 |
Maximum accepted content-webhook body, in bytes |
How the write limiter keys
key: (c) => {
const user = c.get("user");
return user ? `u:${user.id}` : `ip:${clientIp(c)}`;
};
Session resolution runs before the limiter, so a signed-in user gets their own budget rather than sharing one with everyone behind the same NAT.
Read methods — GET, HEAD, OPTIONS — skip this limiter entirely. They are
cheap and safe; the general limiter targets mutations.
One exception: remote-discovery browsing (GET /api/remote/users/:handle,
including its /posts and /recommendations sub-resources) is not cheap — a
cache miss triggers outbound WebFinger, actor, and outbox fetches and writes the
results into your database. Because those requests are unauthenticated read
methods, they would otherwise bypass every limiter. A dedicated RL_REMOTE_MAX
per-IP budget sits on them instead (signed-in users are exempt).
Endpoint-specific limiters (login, registration, inbox) layer stricter caps on top of this backstop.
The inbox path
The federation inbox accepts unauthenticated POSTs from arbitrary instances, so it is defended in three stages:
- Declared length — a
Content-Lengthover the cap is rejected with413before the body is touched at all. - Rate limit — over the per-IP budget returns
429withRetry-After. - Capped buffering — the body is read under a hard cap, so a missing, chunked, or spoofed length cannot stream an unbounded payload into the parser. The request is then rebuilt from the exact same bytes, so the HTTP-Signature digest still verifies.
The defaults are generous on purpose: a busy instance legitimately delivers many activities in a burst.
The content webhook
POST /api/webhooks/content carries no session,
so the general write limiter would bucket every machine caller by IP against the
shared anonymous budget. A tighter per-IP limiter sits on top, and it runs
before the secret is checked — so the endpoint cannot be used to probe for a
valid token at speed.
The body is buffered under WEBHOOK_MAX_BODY_BYTES before it is parsed, the
same defence the inbox uses: a missing or spoofed Content-Length cannot stream
an unbounded payload into the JSON parser.
A CMS publishing a batch is bursty but never fast, so 30/min leaves room for a bulk re-sync while staying far below what a brute-force attempt would need.
Remote discovery (outbound bounds)
Beyond the per-IP RL_REMOTE_MAX budget, the outbound work itself is bounded so
one caller cannot monopolise your instance even within their quota:
- Stricter miss budget — a lookup that actually misses the local cache
(and so performs outbound work) also counts against
RL_REMOTE_MISS_MAX, a tighter per-caller cap. A plain cached read never reaches it; exhausting it is answered with429before any network work starts. - Concurrency —
RL_REMOTE_MAX_OUTBOUNDcaps total in-flight federation lookups;RL_REMOTE_MAX_PER_ORIGINcaps how many a single remote host can occupy, so one slow server cannot starve the rest. - Deadline —
REMOTE_LOOKUP_TIMEOUT_MSaborts a lookup that runs too long, so a slow origin cannot pin a request handler indefinitely. - Coalescing + negative cache — concurrent requests for the same uncached
handle share one lookup, and a failed/not-found resolution is remembered for
REMOTE_NEGATIVE_CACHE_TTL_MSso repeated requests for a missing handle stop re-hitting the origin.
Storage: the remote cache is pruned
Rate limiting and the outbound bounds stop one actor from being repeatedly
abused, but a hostile server serving many distinct, valid actors could still
grow remote_actors and posts on every accepted lookup. The counterweight is
a daily sweep that forgets cached actors which no local user references (via a
follow, mute, block, recommendation, or notification) and which nothing has
re-fetched within REMOTE_CACHE_RETENTION_DAYS (default 30). Deleting an actor
cascades to its posts, so the two grow and shrink together. Set the variable to
0 to keep the cache forever — see
Environment → Rate limiting.
Redis changes the semantics
| Without Redis | With REDIS_URL |
|
|---|---|---|
| Counter scope | Per backend process | Shared across processes |
| Survives restart | No | Yes |
Responses
A limited request returns 429 Too Many Requests with a Retry-After header.
Clients should honour it rather than retrying immediately.
Turning it off
RATE_LIMIT_ENABLED=false
Tuning
- Login too tight for a shared office IP? Raise
RL_LOGIN_MAXrather than disabling the limiter. - An import tool hitting the write cap? Raise
RL_API_WRITE_MAXtemporarily and put it back afterwards. - A large federated instance getting throttled on delivery? Raise
RL_INBOX_MAX; the cost is bounded by the body-size cap. - A writer hitting the upload cap mid-article? Raise
RL_UPLOAD_MAXtemporarily; the per-file size caps are separate and unchanged. - Legitimate remote browsing being throttled? Raise
RL_REMOTE_MAXbefore touching the outbound bounds;RL_REMOTE_MAX_OUTBOUND/RL_REMOTE_MAX_PER_ORIGIN/REMOTE_LOOKUP_TIMEOUT_MSare ceilings on outbound work, not per-viewer budgets, and raising them lifts the load a single slow origin can impose on your instance.
Beyond the rate limit, upload storage is bounded by quotas and garbage collection — see Upload storage.
Apply changes with docker compose up -d.
Found a mistake?Edit this page on GitHub.