This document describes the k6 load testing suite for the TrustLink GraphQL API.
Install k6:
# macOS
brew install k6
# Linux (Ubuntu)
sudo apt-get install k6
# Docker
docker run --rm -it -v $PWD:/scripts grafana/k6:latest run /scripts/k6-load-test.jsFor detailed installation instructions, see k6 Getting Started.
Runs a 5.5-minute scenario with ramping concurrency (10 → 50 → 100 VUs):
cd indexer
npm run test:load
# or with custom base URL:
BASE_URL=http://api.example.com/graphql npm run test:loadQuick 10-second validation that the API is working:
npm run test:load:smokeFor more control, invoke k6 directly:
# Run with custom VU count and duration
k6 run --vus 50 --duration 1m k6-load-test.js
# Run in cloud (requires k6 Pro account)
k6 cloud k6-load-test.js
# Save results to JSON
k6 run --out json=results.json k6-load-test.jsThe load test exercises four representative GraphQL queries:
Queries 10 attestations at a time, useful for dashboard pagination:
query ListAttestations($limit: Int!, $offset: Int!) {
attestations(limit: $limit, offset: $offset) {
id issuer subject claimType timestamp isRevoked metadata
}
}Threshold: p95 < 200ms, p99 < 500ms
Retrieves aggregated stats for a single issuer:
query IssuerStats($issuer: String!) {
issuer(address: $issuer) {
address totalIssued active revoked
}
}Threshold: p95 < 200ms, p99 < 500ms
Lists claim types held by a subject with validity status:
query SubjectClaims($subject: String!) {
subject(address: $subject) {
address
claims { claimType count hasValid }
}
}Threshold: p95 < 200ms, p99 < 500ms
Range queries for historical analysis:
query AttestationsInRange($subject: String!, $from: Int!, $to: Int!, $limit: Int!) {
attestationsInRange(subject: $subject, fromTimestamp: $from, toTimestamp: $to, limit: $limit) {
id claimType timestamp isRevoked
}
}Threshold: p95 < 500ms, p99 < 1000ms
The test follows a gradual ramp-up to detect performance degradation:
| Phase | Duration | VU Count | Purpose |
|---|---|---|---|
| Warm-up | 30s | 0→10 | Cold start behavior |
| Ramp-up | 1m | 10→50 | Linear scaling test |
| Peak | 1m30s | 50→100 | High concurrency |
| Sustained | 2m | 100 | Production load simulation |
| Cool-down | 30s | 100→0 | Graceful shutdown |
Total duration: 5m 30s
Baseline results from a development machine with a local indexer and PostgreSQL:
| Metric | Value | Notes |
|---|---|---|
| Requests/sec (at peak) | 150–300 | Depends on hardware |
| p95 latency | 100–300ms | Most queries return quickly |
| p99 latency | 300–800ms | Range queries slower |
| Error rate | < 5% | Some query errors at peak load |
| Requests total | ~25,000 | Across full 5.5m test |
http_req_duration
- p95 < 200ms ✓
- p99 < 1000ms ✓
- max < 5000ms ? (investigate if exceeded)
http_req_failed
- rate < 5% ✓
- rate > 10% ? (suggests API degradation)
| Category | p95 | Recommendation |
|---|---|---|
| ✅ Excellent | < 100ms | Increase load, add features |
| ✅ Good | 100–300ms | Monitor, plan upgrade before 2x users |
| 300–800ms | Add caching, consider read replicas | |
| ❌ Poor | > 800ms | Scale horizontally or optimize queries |
The load test assumes the following optimizations are in place:
- ✓ PostgreSQL indexes on
issuer,subject,claimType - ✓ GraphQL query batching / dataloader if used
- ✓ Connection pooling (Prisma)
- ✓ Indexer reindex runs at least hourly
If performance degrades, check:
- Database indexes — Run
EXPLAIN ANALYZEon slow queries - Prisma client pool — Ensure connection limit is high enough
- Indexer lag — If reindex is behind, queries hit stale data
- Cache hit rate — Monitor Redis or in-memory cache effectiveness
Based on load test results, use the following rule of thumb:
| Expected QPS | Recommended Resources | Notes |
|---|---|---|
| 100 | 1 vCPU, 2GB RAM | Development |
| 500 | 2 vCPU, 4GB RAM | Small production |
| 2,000 | 4 vCPU, 8GB RAM | Medium production |
| 5,000+ | Horizontal scaling + read replicas | Large production |
To track performance over time:
# Save results to S3 or artifact store
npm run test:load -- --out json=test-$(date +%s).json
# Parse results in CI/CD
cat results.json | jq '.metrics | keys[]'
# Alert if performance regresses
if [ "$(jq '.metrics.http_req_duration.values.p95' results.json)" -gt 500 ]; then
echo "Performance regression detected!"
exit 1
fiIncrease OS file limits:
ulimit -n 65536
npm run test:loadThe test defaults to localhost:4000/graphql. If indexer runs on a different port:
BASE_URL=http://localhost:5000/graphql npm run test:loadEnsure indexer is running:
npm run dev
# In another terminal:
npm run test:loadThe test expects responses within 30s. If queries are very slow:
k6 run --timeout 60s k6-load-test.js