`driver.mjs` is a pipeline benchmark: synthetic images, uniform load, rate limits off, and — the part that mattered — an unconstrained host, so the 1 GB app cap was never exercised and the disk gate never fired. It could not have found either of the two defects fixed in the preceding commits. This harness differs in three ways that earn their keep: REAL CONTENT. Uploads come from a pool of actual wedding photos and videos, unedited, including the HEIC files and 25 MB frames the app is supposed to REFUSE. Those refusals are the test, not noise to filter out — 152 of 932 attempts were refused, and the breakdown of WHY is the most actionable output. PERSONAS. ~100 viewers and ~50 uploaders across nine behaviour profiles, six device profiles, each with a join time and a session length. A casual guest who posts four photos generates a completely different request mix than a photographer dumping 130, and both differ from a kiosk holding one SSE stream all night. A 37-case abuse suite covers malicious payloads, injection, cross-user tampering, enumeration and the rate limiters. RATE LIMITS STAY ON. `driver.mjs` disabled them because it ran every guest from one IP. Almost every limit that matters is per USER, not per IP, and those are as real for 150 synthetic sessions as for 150 phones — leaving them on is what lets the abuse personas prove the defences work. The per-IP limits ARE distorted by the single source address; that distortion is measured and reported rather than configured away. `docker-compose.sim.yml` reproduces the CX22 rather than asserting it: production's per-service cpus/memory/cpu_shares verbatim, every container pinned to the SAME two cores with `cpuset` so they genuinely contend, and a real loopback ext4 volume so the app's own statvfs returns true numbers. Run the driver under `taskset` onto other cores, or the load generator competes with the thing it measures. `browser-check.mjs` covers what an HTTP driver structurally cannot: the SvelteKit container, and whether the frontend ESCAPES the XSS caption the backend stores verbatim. The backend stores captions raw by design, so the renderer is the entire defence — and only a browser can prove the payload is inert. It reports INCONCLUSIVE rather than PASS when the payload never reached the DOM, because a check that renders nothing proves nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EventSnap load / stress test
Simulates a real event: ~100 guests joining and uploading ~1000 images in
bursts (10–20 at a time) spread across a compressed time window, a pool of
viewers holding live SSE connections, and one real browser on /diashow
acting as the showcase display.
Goal (this harness is tuned for it)
Validate the shipping config. We run at the real production defaults —
compression concurrency (COMPRESSION_WORKER_CONCURRENCY, default 2), DB
pool (default 10), quotas on — and answer: does the app survive the
event, and how far behind real-time does the diashow fall?
The headline metric is pipeline latency: time from an upload succeeding to
its preview being ready (upload-processed SSE event) — i.e. how long until the
photo appears on the diashow. A backlog that builds is fine; a backlog that
never drains is a fail for a live event.
Methodology: what we change vs. shipping
We only disable rate limits. They're per-IP / per-user anti-abuse guards; a synthetic test from one IP trips them in a way real guests (distinct IPs, phones) never would — leaving them on would measure the limiter, not the pipeline. Everything else (compression concurrency, DB pool, quotas) stays at the real default so the result is honest.
Standalone finding to remember: the shipping
upload_rate_per_hourdefault is 10. A real guest uploading a burst of 10–20 photos would be throttled by the shipping config too. That's a genuine event-day issue worth surfacing separately from this pipeline test.
Prereqs
- The isolated test stack up:
cd e2e && npm run stack:up(Caddy on:3101,EVENTSNAP_TEST_MODE=1,/admin/__truncatelive). - Node 24+ (global
fetch/FormData/Blob), Python 3 + Pillow, Docker CLI access (used fordocker stats+docker exec psqlground-truth sampling). @playwright/test(already an e2e dep) for the diashow watcher.
1. Generate the image pool (once)
Realistic phone-sized JPEGs (~2–4 MB, 12 MP, high entropy). The driver reuses this pool at random across all 1000 uploads — real load is byte size + decode cost, not file uniqueness.
python3 e2e/loadtest/gen-images.py 40 # → /tmp/eventsnap-loadtest/photos
~40 images ≈ 120 MB pool; projects to ~3–4 GB of originals for 1000 uploads (previews/thumbnails add more). The generator prints the projection; check disk.
2. Smoke run first (~1 min)
Proves the wiring — join, upload, SSE correlation, drain, metrics — before the real thing:
LT_GUESTS=5 LT_IMAGES=50 LT_WINDOW_SEC=60 node e2e/loadtest/driver.mjs
3. Full run (~15 min + drain)
Two terminals. Start the showcase display first, then the driver:
# terminal A — the showcase device
node e2e/loadtest/diashow-watch.mjs
# terminal B — 100 guests / 1000 images / 15-min window (defaults)
node e2e/loadtest/driver.mjs
The driver truncates event data first (LT_TRUNCATE=0 to keep), disables rate
limits, joins guests, opens SSE, runs the burst schedule, then waits for the
compression backlog to drain before reporting.
Output
- Console: live progress every 10 s, then a RESULTS block with pass/fail flags.
e2e/loadtest/results/run-<timestamp>.json: full metrics — upload latency percentiles, pipeline latency percentiles, drain time, per-status counts, SSE reconnect/resync counts, and adocker stats+ DB-connection time series.e2e/loadtest/results/diashow/: periodic screenshots of the live display.
What the flags mean
| Flag | Meaning |
|---|---|
✗ 5xx |
server errored under load — hard fail |
✗ 507 |
quota rejected uploads — disk/quota misconfig for the event size |
✗ backlog did not drain |
compression can't keep up even after uploads stop — diashow never catches up |
⚠ pipeline p95 > 60s |
photos take >1 min to appear on the diashow at peak |
⚠ SSE resyncs |
live consumers lagged the broadcast channel |
Knobs
All via env (see header of driver.mjs): LT_GUESTS, LT_IMAGES,
LT_WINDOW_SEC, LT_BURST_MIN/MAX, LT_BURST_CONC, LT_VIEWERS,
LT_TRUNCATE, LT_DRAIN_TIMEOUT_SEC, LT_KEEP_RATELIMITS, LT_BASE,
LT_APP_CONTAINER, LT_DB_CONTAINER.
To later answer "what config should I deploy?", re-run with a rebuilt stack
that sets COMPRESSION_WORKER_CONCURRENCY higher (boot-time env var in
docker-compose.test.yml) and compare the pipeline-latency / drain numbers.
Teardown
cd e2e && npm run stack:down # wipes volumes (media + db)