Skip to content

Rate limits

uwwoe enforces per-IP rate limits on state mutations as a soft DoS defence. The limits are deliberately conservative; if you hit them in normal use, file an issue.

What’s limited

Endpoint familyLimited?Why
POST /api/v1/state (mint)YesToken-mint flood is the easiest abuse vector.
PATCH/PUT /api/v1/state/currentYesWrite contention against the SQLite serialized writer.
DELETE /api/v1/state/currentYesCheap server-side but easy to flood.
POST /api/v1/state/current/migration-previewYesCompute-heavy.
GET /api/v1/state/current, /state/current/exportSoftReads are cheap; capped at a much higher threshold.
POST /api/v1/query/*Not currentlyThe direct evaluator is bounded; future heavier engines will gate this.
POST /api/v1/graph/views/*Not currentlyBounded responses cap per-request cost.
GET /api/v1/courses, /credentials, /source-references/*, /index, /healthNoCheap catalog reads.

Tuning knobs

The production deploy reads these from the environment:

Env varMeaning
UWSCRAPE_RATE_LIMIT_STATE_PER_IP_PER_MINUTEToken-bucket refill rate for state mutations per IP.
UWSCRAPE_RATE_LIMIT_STATE_PER_IP_BURSTBurst capacity for state mutations per IP.
UWSCRAPE_TRUST_PROXY_HEADERSWhether to extract client IP from X-Forwarded-For (true on Fly).

Operators tune these to match the deployment’s user concurrency profile; the defaults target the P10 capacity goal of ~50–200 concurrent users.

429 response shape

{
"error": {
"code": "rate_limit_exceeded",
"message": "Per-IP state mutation rate limit exceeded.",
"details": {
"limiter": "state_per_ip_per_minute",
"retry_after_seconds": 12
}
},
"meta": { "api_version": "v1", "request_id": "req_..." }
}

The HTTP Retry-After header is also set to the same number of seconds.

What “per IP” actually means

Behind Fly’s edge, the client IP is extracted from the Fly-Client-IP header (or X-Forwarded-For when UWSCRAPE_TRUST_PROXY_HEADERS=true). Shared NATs and CGNAT residential ISPs can collapse multiple real users to one rate-limit bucket — if you suspect this is happening, the diagnostic endpoint surfaces the resolved client address.

Limits and the deletion-indistinguishability guarantee

The 429 response does not leak whether the supplied token would have authenticated — the rate-limit check happens before the auth check, and the response shape is the same regardless of token validity. A hard-deleted token does not become distinguishable from an unknown-but-not-yet-rate-limited token. (ADR 0019)

Caveats

  • Rate limits are in-process on a single Fly machine. If you scale horizontally (currently you can’t; ADR 0010 mandates one machine), the limit must move to a shared store.
  • Limits do not yet apply to POST /api/v1/query/*. When future engine adapters (Datalog, SAT, bounded SMT/CP per ADR 0024) land, query rate limits will be necessary.

Want more?