Status: Active Derives from: ADR 0002 — Initial Architecture (initial budgets in 04-technical-architecture.md § Performance Budgets), ADR 0005 — Live Classroom Media Stack (classroom join + bandwidth budgets). Public-read additions derive from ADR-0052; P02d-5's bounded caller and dynamic/no-store policy derive from ADR-0053.
Performance budgets, the rules that keep them, and the test discipline that protects them.
P02d-7 measurement decision — Accepted 2026-10-11.
ADR-0056
defines the browser/Lighthouse measurement contract and hard/advisory split.
Its normalized gzip definition covers same-/cross-origin Script responses from
cold navigation through collection completion, including initial async/lazy
loads and original-document inline executable bodies. It excludes inert data,
evaluation-generated code and scripts requested after that window or by later
interaction. This can be stricter than entry-chunk/build-time accounting; it is
not merely a unit clarification. P7 delivers the always-running
public demo + lighthouse job and its fail-closed browser/report/source controls.
The hard gzip/CLS/automated-accessibility checks make this standard Active for
that delivered scope. Timing medians remain advisory; production baselines,
field telemetry and load tests remain Phase 11. The
Step 4 record
owns real CI receipts and bounded required-check activation.
| Surface | Budget |
|---|---|
| Public landing page (TTFB) | < 200 ms server |
| Public landing page (LCP) | < 1.5 s on fast 4G |
| Course catalog server response | < 300 ms |
| Lesson player initial load | < 500 ms |
| API p95 (read) | < 200 ms |
| API p95 (write) | < 500 ms |
| API p99 (read) | < 500 ms |
| Classroom join time (token + connect) | < 1.5 s |
| Frontend INP (interaction-to-next-paint) | < 200 ms |
| Frontend CLS (cumulative layout shift) | < 0.05 |
| Cold start (worker, after restart) | < 5 s |
Budgets are reviewed quarterly against measured production metrics.
- Index every foreign key.
- Index
tenant_idas the first column of composite indexes for tenant-owned tables. - Avoid N+1 — projection (
Select(...)) preferred overIncludechains. - Avoid
AsTrackingfor read-only queries. - Slow queries (> 500 ms) logged with
slow_query=trueand reviewed weekly. - Each module's hottest read paths must have a covering index.
- Read-through cache for stable, read-heavy data except the P02d-4 public API boundary below; renderer-cache choices remain separately owned.
- Cache invalidation triggered by integration events from the producing module.
- Cache keys carry the tenant, the organization where applicable, and the locale (Security Standards § Multi-Tenant + Organization Isolation Review Checklist).
- TTL chosen per content type; default 5 minutes for catalog, 1 minute for course detail.
- Cache hit ratio per cache name surfaced as a metric.
Customization's internal untranslated definition cache is a generation-driven
exception to event/locale invalidation under ADR-0043 and P02d-3's Accepted G22
answer. Its architecture owner
requires a fresh SQL generation probe; < 1 ms measures in-memory resolution
only, not end-to-end latency. Cold/partial/fault reads use at most two SELECTs.
Seeded timings and plans do not establish a production p95.
P02d-4 Accepted exception — 2026-10-03. Step 2 delivers the early
public-prefix response policy; Step 3 delivers Education reads and records actual
app-role query plans and local HTTP samples in its delivery record. Every
/api/v1/public response, including middleware/errors, uses Cache-Control: no-store.
No site/Education representation cache, ETag, Last-Modified or 304 is introduced;
internal generation-keyed definitions remain independent. Record actual app-role
consumer query plans and representative local cold/warm measurements without a
production p95 claim. P02d-5/G37 accepts dynamic/no-store public rendering under
ADR-0053; no shared Next
representation cache is introduced. Internal definition caches remain independent.
The accepted package
owns that boundary under ADR-0052.
- All list endpoints are paginated as
API Standards § Pagination specifies; that section owns
the default and maximum
limit, the answer to an absentlimit, and when offset pagination is allowed.
- Anything > 200 ms server time that does not need to block the response is moved to a Hangfire job.
- Job duration p95 monitored; jobs > 30 s have a long-running designation and a dedicated queue.
- Long-running jobs are checkpointable / resumable when possible.
- Every outbound provider call has a timeout (≤ 10 s for synchronous, ≤ 60 s for jobs).
- Retries with exponential backoff for transient failures.
- Circuit breaker pattern for provider outages.
- Outbound calls are not made inside DB transactions.
- Avoid loading whole result sets when streaming would do.
- Stream large file uploads to SeaweedFS/S3; never buffer the whole file in memory.
- Avoid string concatenation in tight loops; use
StringBuilderor pooled buffers.
- Initial JS payload on a public route: < 200 decimal KB (
< 200000bytes), measured using ADR-0056's normalized source scope. - Studio routes may exceed this; budget reviewed per route.
- Lazy load below-the-fold blocks and modals.
- Code-split heavy client libraries (rich-text editor, video player) via
dynamic(...).
- Server Components by default.
- Streaming with
<Suspense>to ship hero content first. - Parallel data fetches in RSC via
Promise.all. - Avoid sequential request waterfalls.
next/imageeverywhere.- Explicit
width/heightto prevent layout shift. - Use modern formats (AVIF, WebP) where the browser supports them.
priorityfor above-the-fold hero images only.
next/fontself-hosted.- Subset to the languages actually used.
font-display: swap.
- Web Vitals collected via the Next.js reporting hook.
- Dashboards track LCP, INP, CLS, FCP per route.
- Regression on a critical route is a Sev-2 issue.
Accepted P02d-5 G35. Web-vitals reporting belongs to Phase 11; P02d-5 provides no hook, per ADR-0053. Observability Standards § Frontend Observability records that ownership.
- Join time < 1.5 s p95.
- Token issuance < 200 ms.
- LiveKit SFU node sized per 12-infrastructure.md — ~250 concurrent participants per 2 vCPU.
- Egress workers separate from the SFU node.
- TURN traffic monitored; > 30% sustained triggers a network review.
Required load tests (Phase 11 deliverable):
- Public landing page at 1k RPS.
- Course catalog list at 500 RPS.
- Login + session at 100 RPS.
- Classroom join at 50 RPS.
- Recording start/stop at 10 RPS.
Tests run against staging weekly and before a major launch.
- Quarterly performance review meeting.
- Top 10 slowest endpoints inspected.
- Top 10 most-expensive queries inspected.
- Bundle size deltas reviewed.
- Mobile Core Web Vitals reviewed.
- Adding a route without a pagination cap.
- Adding a list endpoint without an index supporting it.
- Shipping a public route bundle > 250 KB gzipped without an ADR.
- Calling external providers inside a database transaction.
- Loading whole tables into memory ("just to filter in code").
- Building dashboards that query without
tenant_idas the first WHERE column.