Standalone, replicable benchmark harness for the Delegation Receipt Standard (DRS). It measures the published DRS stack end to end, the way real users run it — this repo imports no code from the DRS source tree:
- Host —
ghcr.io/okeyamy/drs-verify(pinned by digest, = release v0.1.1) + Redis 7.4, viacompose.bench.yml. Nothing is built locally. - Devs — 15 simulated developers, each with their own Ed25519 keypair,
did:keyidentity, and delegation chain, issued with the published@okeyamy/drs-sdkfrom npm. - Load — k6 POSTs pre-signed bundles to
POST /verify. Every request carries a unique JTI; each dev arrives from a distinct client IP (X-Forwarded-For+TRUST_PROXY=true), so per-IP rate limiting sees 15 clients, not one load generator.
- Docker (compose v2 plugin or standalone docker-compose v1 — both work)
- Node.js ≥ 20
- k6 ≥ v1 (tested with v2.0.0)
- pnpm (or npm) to install the SDK
git clone https://github.com/OkeyAmy/drs-bench
cd drs-bench
pnpm install # pulls @okeyamy/drs-sdk from npm
./run.sh # all scenarios (~12 min)
./run.sh s2 # or a single scenarioRaw output lands in results/: k6 summaries (*.json, *.log),
/metrics snapshots before/after each scenario, container CPU/mem samples,
and the machine description. Curated numbers go in RESULTS.md.
| # | What | Why |
|---|---|---|
| S0 | Fixture generation itself | SDK issuance throughput (single process, real signatures) |
| S1 | 1 dev, 200 sequential requests at chain depth 1 / 4 / 16 | Floor latency; CPU scaling with chain depth |
| S2 | 15 devs, 300 rps steady, 60 s, depth 4, Redis | Realistic multi-tenant load |
| S3 | Ramp 100 → 1500 rps, depth 4 | Saturation point of one instance |
| S4 | 300 rps with 10 % replayed JTIs | Replay storm: 409s must stay cheap |
| S5 | S2 with in-memory nonce backend | Isolates the Redis roundtrip cost |
| S6 | 300 rps through an org tool server (bundle in X-DRS-Bundle header, fail-closed gate, tool executes only on valid + binding:"match") |
The full canonical topology — what a dev actually experiences per tool call |
- Fixtures expire in 15 minutes.
INVOCATION_STALEfires whennow − iat > NONCE_STORE_TTL_SECS(900 s in the bench config), sorun.shregenerates fixtures immediately before each scenario. - Unique JTI per request. The nonce store burns each JTI on first use (409 on reuse), so the k6 script indexes bundles by global iteration number and never resends one — except deliberately, in S4.
- Rate limits are raised (
RATE_LIMIT_PER_IP=1000,RATE_LIMIT_GLOBAL=10000; defaults 100/1000). At defaults, the global limiter answers 429 at 1000 rps before the verifier is working hard — the bench measures the verifier, not the token bucket. - Bundles must fit the 64 KiB body cap. The generator asserts this; a depth-16 bundle measures ~12 KiB.
- Issuance is single-threaded at ~290 bundles/s per Node process
(measured with
spike-issuance-rate.mjs), so generation shards across 5 parallel processes.
Numbers are machine-relative — compare scenarios against each other on the
same machine, not against another machine's absolute values.
results/machine.txt records the hardware for each run.