ADR 0039 — Scheduler Quotas and Limits Under the Entitlement Model
Status: Accepted Context date: 2026-08-06 Builds on: ADR 0028 (soft gates, 402-vs-403 distinction), ADR 0030 (entitlement classes and projections)
Context
ADR 0030 defines three enforcement classes — quotas (metered consumables, billing-reconciled, projected verdicts), limits (stock ceilings, counted and enforced synchronously by the owning service), and features (projected flags) — and the rule that no service ever calls billing synchronously. The scheduler's dimensions had to be sorted into those classes before implementation, because the class determines the architecture: what the scheduler counts itself versus what it merely projects.
Decision
1. Dimension classification
| Dimension | Class | Enforced |
|---|---|---|
max_schedules | limit | Scheduler counts its own schedule rows, synchronously, at create — read-your-writes against the primary. |
min_schedule_interval_seconds | limit | Validated at create/update against the entitlement snapshot (e.g. Free 3600 s, Scale 60 s). |
max_payload_bytes | limit | Validated at create (≤ 64 KiB hard cap regardless of plan; the plan may set less). |
max_concurrent_runs_per_tenant | limit | Executor semaphore — Redis token bucket, reconstructible, never authoritative. |
scheduled_runs_per_cycle | quota | Billing reconciles usage; the scheduler reads a projected verdict and marks over-quota occurrences skipped(reason=quota_exhausted). |
No synchronous call to billing, ever — the ADR 0030 rule holds without exception.
2. The quota is a soft gate, per ADR 0028
When quota_blocked in the tenant_entitlements projection contains scheduled_runs_per_cycle, the planner still materialises occurrences but marks them skipped(reason=quota_exhausted) — visible rows in run history and a schedule.run.skipped event, never a silent stop and never a suspension. This is ADR 0028's posture applied to schedules: a recoverable quota stops new consumption while the tenant can observe, top up, and resume; schedules and their configuration are untouched.
3. Projection mechanics
The scheduler-entitlements consumer group (ADR 0038) maintains tenant_entitlements (pinned snapshot + quota_blocked[]), ordered by the monotonic entitlement_version guard (WHERE excluded.entitlement_version > stored) — ADR 0030's clock-free ordering, not timestamp comparison. Gates fail-open on a missing projection row (unmanaged tenant) with an alertable counter, matching the platform-wide posture (ADR 0025/0028/0030).
Alternatives Considered
- Synchronous billing check at plan/fire time. Rejected: the standing platform rule (ADR 0026/0028/0030) — it couples the hottest loop to billing availability and adds a network hop per occurrence.
- Hard-failing schedule creation when over the run quota. Rejected: the quota is on runs per cycle, a consumable; blocking configuration changes because a consumable ran out conflates ADR 0030's classes (limits gate creation, quotas gate consumption).
- Suspending or auto-pausing schedules on quota exhaustion. Rejected: overloading pause/suspension states with billing verdicts is the exact bug ADR 0028 fixed for rooms (402 vs 403). The schedule stays
active; its occurrences are skipped with a reason, and everything resumes without tenant action when the verdict clears. - Scheduler counting its own runs against the quota locally. Rejected: the quota's period, allowance, and top-up arithmetic live in billing's ledger (ADR 0030); duplicating the verdict computation in the scheduler creates two arbiters of one number. The scheduler owns limits (its own row counts), billing owns quotas.
Consequences
- Positive: Every enforcement follows an existing, proven pattern — nothing about the scheduler required a new entitlement mechanism, which is evidence the ADR 0030 classes are correctly drawn.
- Positive: Over-quota behaviour is fully auditable: each skipped occurrence is a row with
reason=quota_exhausted, answerable via the run-history API and the emitted skip event. - Negative / accepted: Quota enforcement lags by the reconcile-and-project pipeline (seconds); a tenant crossing the line mid-cycle gets a few over-quota runs. Accepted platform-wide by ADR 0030.
- Negative / accepted: Fail-open on projection gaps means a brand-new managed tenant may briefly run unmetered until the first entitlement event projects — bounded by at-least-once redelivery, same as every other projection gate.
- Follow-up: The plan catalog (ADR 0030 §3 dimension registry) gains the scheduler dimensions; per-tier values are a catalog seeding change, not a scheduler change.