ADR-0010 — Public triggers are rate-limited, capped, and shared¶
- Status: accepted
- Date: 2026-07-02
- Domain: security · exhibit mechanics
Context¶
The exhibits put unauthenticated POST endpoints on the public internet ("click to run a comparison", "inject a fault"). Anything clickable is scriptable: a public trigger can be hammered by bots, curl loops, or an enthusiastic recruiter's toddler. The box has 2 vCPU and shares them with everything else. Securing the feature is not overhead — it generates architect signal (a public interactive surface, reasoned about as an attack surface).
Decision¶
Three layers, all server-side:
- Rate limiting on every POST: per-IP (suggested 10/min) and global caps, answered with
429, aRetry-Afterheader, and a JSON body carryingretry_after_seconds(contract shape indocs/contracts/lab-api-contract.md). - Hard caps in the services themselves, independent of the limiter: max window length (180 s), max windows/hour (6), fault auto-heal (60 s, fixed deadline, not extendable by repeated POSTs). Even a limiter bug cannot make the box run unbounded work.
- Shared-run semantics: if a run window is already active, a new trigger joins it
(
joined: true) instead of starting another or queueing. Concurrent viewers watch the same run; the host never executes two. Same for fault state: one global demo state, shared by all viewers, documented on the page.
Consequences¶
- Worst-case load is bounded by design, not by hope: one streaming window, one fault state, one benchmark run at any moment.
- Shared state means one visitor can change what another visitor is watching. Accepted and disclosed — for a demo this is a feature (it proves it's live), not a bug.
- The front-end handles 429 gracefully (shows the retry delay), so the limit is part of the UX, not an error state.
Alternatives considered¶
- Per-viewer isolated runs — rejected: multiplies load by audience size, exactly what the host cannot do.
- Auth-gating the triggers — rejected: kills the point; a recruiter must be able to poke it with zero friction.
- CAPTCHA — rejected: friction without meaningfully more protection than the caps already give.