Skip to content

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:

  1. Rate limiting on every POST: per-IP (suggested 10/min) and global caps, answered with 429, a Retry-After header, and a JSON body carrying retry_after_seconds (contract shape in docs/contracts/lab-api-contract.md).
  2. 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.
  3. 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.