Skip to content
Omicron

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:

  1. Declared length — a Content-Length over the cap is rejected with 413 before the body is touched at all.
  2. Rate limit — over the per-IP budget returns 429 with Retry-After.
  3. 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 with 429 before any network work starts.
  • Concurrency — RL_REMOTE_MAX_OUTBOUND caps total in-flight federation lookups; RL_REMOTE_MAX_PER_ORIGIN caps how many a single remote host can occupy, so one slow server cannot starve the rest.
  • Deadline — REMOTE_LOOKUP_TIMEOUT_MS aborts 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_MS so 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_MAX rather than disabling the limiter.
  • An import tool hitting the write cap? Raise RL_API_WRITE_MAX temporarily 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_MAX temporarily; the per-file size caps are separate and unchanged.
  • Legitimate remote browsing being throttled? Raise RL_REMOTE_MAX before touching the outbound bounds; RL_REMOTE_MAX_OUTBOUND / RL_REMOTE_MAX_PER_ORIGIN / REMOTE_LOOKUP_TIMEOUT_MS are 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.