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 family | Limited? | Why |
|---|---|---|
POST /api/v1/state (mint) | Yes | Token-mint flood is the easiest abuse vector. |
PATCH/PUT /api/v1/state/current | Yes | Write contention against the SQLite serialized writer. |
DELETE /api/v1/state/current | Yes | Cheap server-side but easy to flood. |
POST /api/v1/state/current/migration-preview | Yes | Compute-heavy. |
GET /api/v1/state/current, /state/current/export | Soft | Reads are cheap; capped at a much higher threshold. |
POST /api/v1/query/* | Not currently | The direct evaluator is bounded; future heavier engines will gate this. |
POST /api/v1/graph/views/* | Not currently | Bounded responses cap per-request cost. |
GET /api/v1/courses, /credentials, /source-references/*, /index, /health | No | Cheap catalog reads. |
Tuning knobs
The production deploy reads these from the environment:
| Env var | Meaning |
|---|---|
UWSCRAPE_RATE_LIMIT_STATE_PER_IP_PER_MINUTE | Token-bucket refill rate for state mutations per IP. |
UWSCRAPE_RATE_LIMIT_STATE_PER_IP_BURST | Burst capacity for state mutations per IP. |
UWSCRAPE_TRUST_PROXY_HEADERS | Whether 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?
- Errors — full error code list.
- Production deploy (P10) — full deploy posture.