Performance and load testing infrastructure for the in-midst-my-life API using k6.
macOS (Homebrew):
brew install k6
Linux (Debian/Ubuntu):
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
Windows (Chocolatey):
choco install k6
Docker:
docker pull grafana/k6
k6 version
# k6 v0.50.0 (or similar)
k6/
├── config.js # Shared configuration and utilities
├── scenarios/
│ ├── smoke.js # Quick health check (CI/CD)
│ └── baseline.js # Comprehensive load testing
└── README.md # This file
# From repository root
pnpm dev
# Or just the API
pnpm --filter @in-midst-my-life/api dev
k6 run infra/k6/scenarios/smoke.js
k6 run infra/k6/scenarios/baseline.js
smoke.js)Quick health check for CI/CD pipelines. Validates critical endpoints are responsive.
# Default run (1 VU, 30s)
k6 run infra/k6/scenarios/smoke.js
# Custom base URL
K6_BASE_URL=https://api.staging.example.com k6 run infra/k6/scenarios/smoke.js
Endpoints tested:
GET /health - Health checkGET /ready - Readiness probeGET /v1/taxonomy/masks - Taxonomy masksGET /v1/taxonomy/epochs - Taxonomy epochsGET /v1/taxonomy/stages - Taxonomy stagesExpected results:
baseline.js)Comprehensive load testing with multiple scenario presets.
# Run default baseline (10 VUs, 1m)
k6 run infra/k6/scenarios/baseline.js
# Run specific scenario
k6 run --env SCENARIO=smoke infra/k6/scenarios/baseline.js
k6 run --env SCENARIO=load infra/k6/scenarios/baseline.js
k6 run --env SCENARIO=stress infra/k6/scenarios/baseline.js
k6 run --env SCENARIO=spike infra/k6/scenarios/baseline.js
k6 run --env SCENARIO=soak infra/k6/scenarios/baseline.js
Scenario presets:
| Scenario | VUs | Duration | Purpose |
|---|---|---|---|
smoke |
1 | 30s | Quick validation |
baseline |
10 | 1m | Normal expected load |
load |
50 | 5m | Sustained moderate traffic |
stress |
0→200 | 7m | Find breaking point (ramping) |
spike |
10→200→10 | 3m | Sudden traffic burst |
soak |
30 | 30m | Memory leak detection |
Endpoints tested (with profile):
GET /v1/profiles/:id - Profile retrievalPOST /v1/profiles/:id/narrative - Narrative generationPOST /v1/profiles/:id/masks/select - Mask selection| Variable | Default | Description |
|---|---|---|
K6_BASE_URL |
http://localhost:3001 |
API base URL |
K6_API_VERSION |
v1 |
API version prefix |
K6_PROFILE_ID |
00000000-0000-0000-0000-000000000001 |
Test profile ID |
K6_AUTH_TOKEN |
(empty) | JWT auth token |
K6_TIMEOUT |
30000 |
Request timeout (ms) |
K6_VERBOSE |
false |
Enable verbose logging |
SCENARIO |
baseline |
Scenario preset (baseline.js) |
# Local development
k6 run infra/k6/scenarios/smoke.js
# Staging environment with auth
K6_BASE_URL=https://api.staging.example.com \
K6_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIs... \
k6 run infra/k6/scenarios/baseline.js
# Stress test with custom profile
K6_PROFILE_ID=abc-123-def-456 \
SCENARIO=stress \
k6 run infra/k6/scenarios/baseline.js
# Docker execution
docker run --rm -i \
-e K6_BASE_URL=http://host.docker.internal:3001 \
-v $(pwd)/infra/k6:/scripts \
grafana/k6 run /scripts/scenarios/smoke.js
| Metric | Threshold | Description |
|---|---|---|
http_req_duration p(50) |
< 200ms | Median latency |
http_req_duration p(95) |
< 500ms | 95th percentile latency |
http_req_duration p(99) |
< 1000ms | 99th percentile latency |
http_req_failed |
< 1% | Error rate |
health_latency p(95) |
< 100ms | Health endpoint latency |
taxonomy_latency p(95) |
< 300ms | Taxonomy endpoint latency |
profile_latency p(95) |
< 500ms | Profile endpoint latency |
narrative_latency p(95) |
< 2000ms | Narrative generation latency |
| Metric | Threshold | Description |
|---|---|---|
http_req_duration p(50) |
< 500ms | Median latency (relaxed) |
http_req_duration p(95) |
< 2000ms | 95th percentile (relaxed) |
http_req_duration p(99) |
< 5000ms | 99th percentile (relaxed) |
http_req_failed |
< 5% | Error rate (relaxed) |
These are reference results from a local development environment. Your results may vary based on hardware and configuration.
scenarios: (100.00%) 1 scenario, 1 max VUs, 1m0s max duration
✓ health: status is 200
✓ health: body contains ok
✓ health: response time < 100ms
✓ ready: status is 200 or 503
✓ masks: status is 200
✓ masks: returns array
✓ epochs: status is 200
✓ stages: status is 200
checks.........................: 100.00%
http_req_duration...............: avg=12ms min=3ms med=8ms max=45ms p(95)=32ms
http_req_failed.................: 0.00%
scenarios: (100.00%) 1 scenario, 10 max VUs, 1m30s max duration
✓ http_req_duration p(95) < 500ms
✓ http_req_failed < 1%
✓ health_latency p(95) < 100ms
✓ taxonomy_latency p(95) < 300ms
http_reqs......................: 1200 20/s
http_req_duration...............: avg=45ms min=2ms med=35ms max=350ms p(95)=120ms
http_req_failed.................: 0.10%
scenarios: (100.00%) 1 scenario, 50 max VUs, 5m30s max duration
✓ http_req_duration p(95) < 500ms
✓ http_req_failed < 1%
http_reqs......................: 15000 50/s
http_req_duration...............: avg=85ms min=3ms med=60ms max=800ms p(95)=280ms
http_req_failed.................: 0.50%
# .github/workflows/load-test.yml
name: Load Test
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6am
jobs:
load-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
- name: Run migrations and seed
env:
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/midst_test
REDIS_URL: redis://localhost:6379
run: |
pnpm --filter @in-midst-my-life/api migrate
pnpm --filter @in-midst-my-life/api seed
- name: Start API server
env:
POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/midst_test
REDIS_URL: redis://localhost:6379
run: |
pnpm --filter @in-midst-my-life/api start &
sleep 10 # Wait for server
- name: Install k6
run: |
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
- name: Run smoke test
run: k6 run infra/k6/scenarios/smoke.js
- name: Run baseline test
run: k6 run infra/k6/scenarios/baseline.js
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: k6-results
path: |
*.json
*.html
# JSON output for programmatic processing
k6 run --out json=results.json infra/k6/scenarios/baseline.js
# CSV output for spreadsheet analysis
k6 run --out csv=results.csv infra/k6/scenarios/baseline.js
# Cloud output (Grafana Cloud k6)
K6_CLOUD_TOKEN=your-token k6 cloud infra/k6/scenarios/baseline.js
# InfluxDB output (for Grafana dashboards)
k6 run --out influxdb=http://localhost:8086/k6 infra/k6/scenarios/baseline.js
# Prometheus Remote Write
k6 run --out experimental-prometheus-rw infra/k6/scenarios/baseline.js
Import the dashboard at infra/grafana/dashboards/api-metrics.json to visualize:
docker-compose up prometheus grafana
k6 run --out experimental-prometheus-rw infra/k6/scenarios/baseline.js
http://localhost:3000 and import the dashboard.# Check if API is running
curl http://localhost:3001/health
# Check Docker network (if using containers)
docker network ls
docker inspect <container_id>
# Seed the database first
pnpm --filter @in-midst-my-life/api seed
# Or specify a different profile ID
K6_PROFILE_ID=your-profile-id k6 run infra/k6/scenarios/baseline.js
# Get a valid JWT token
# Then run with auth
K6_AUTH_TOKEN=eyJhbG... k6 run infra/k6/scenarios/baseline.js
# Increase timeout for slow endpoints
K6_TIMEOUT=60000 k6 run infra/k6/scenarios/baseline.js
Create new test files in scenarios/:
// scenarios/custom.js
import http from 'k6/http';
import { check } from 'k6';
import { config, defaultParams, buildUrl } from '../config.js';
export const options = {
vus: 5,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const res = http.get(buildUrl('/your-endpoint'), defaultParams);
check(res, {
'status is 200': (r) => r.status === 200,
});
}
Run with:
k6 run infra/k6/scenarios/custom.js