Home
operator notes / source-backed load profile
Run the Rinha load suite without guessing what it measures.
A Docker-first Grafana k6 harness for Rinha de Backend 2024/Q1. It runs five banking API scenarios, records per-scenario Trend metrics, and switches between quiet CI-friendly k6 output and live InfluxDB output.
What this repository provides
rinha2-back-end-k6 is the shared stress-test harness for the Rinha de Backend 2024/Q1 fictional banking API challenge. It packages a custom k6 binary with xk6-output-influxdb, the scenario script, and a small shell entrypoint into a reusable Docker image.
The docs are intentionally source-backed. Scenario counts, VU targets, metric names, defaults, and run modes should match test/stress-test/rinha-test.js and test/stress-test/run-test.sh.
Quick start
Build and run the image locally:
docker build -t rinha-k6 .
docker run --rm \
-e MODE=prod \
-e BASE_URL=http://api:9999 \
rinha-k6
Use dev mode when you have an InfluxDB endpoint ready for k6 metrics:
docker run --rm \
-e MODE=dev \
-e BASE_URL=http://api:9999 \
-e K6_INFLUXDB_ADDR=http://influxdb:8086 \
rinha-k6
run-test.shtreatsMODE=devor an emptyMODEas InfluxDB export mode. SetMODE=prodexplicitly when you want the quiet CI path (k6 run ... --quiet).
Scenario map
validacoes5 VUs, one passcliente_nao_encontrado1 VU, 404 checkdebitos1 to 220 VUscreditos1 to 110 VUsextratos10 VUs, statement readRepository structure
rinha2-back-end-k6/
├── test/stress-test/
│ ├── rinha-test.js # Main k6 file, 5 scenarios and 5 Trend metrics
│ └── run-test.sh # MODE dispatcher plus 15s startup delay
├── Dockerfile # Go 1.25/xk6 Alpine builder plus Alpine 3.23 runtime
├── docs/wiki/ # Source Markdown for the public docs routes
├── docs/ # Astro docs site published to GitHub Pages
└── .github/workflows/
├── main-release.yml # Build and push GHCR image
├── deploy.yml # Reusable GitHub Pages deploy workflow
└── codeql.yml # JavaScript CodeQL analysis
Docker image
The release workflow publishes a multi-platform image to GitHub Container Registry:
ghcr.io/jonathanperis/rinha2-back-end-k6:latest
Sibling Rinha implementations can run the same test image against their own BASE_URL so the load profile stays consistent across languages and backends.
Source-backed audit notes
Current facts verified against the code and workflows:
rinha-test.jsexports exactly five scenarios and five Trend metrics.run-test.shprints a 15-second startup warning before it chooses a mode.prodmode usesk6 run rinha-test.js --quiet; no HTML artifact is generated by this repository entrypoint.devmode, including emptyMODE, usesk6 run rinha-test.js -o xk6-influxdb..github/workflows/main-release.ymlpublishes onlyghcr.io/jonathanperis/rinha2-back-end-k6:latestforlinux/amd64andlinux/arm64/v8.
Key design patterns
- SharedArray client data: client IDs and limits are loaded once and shared across VUs.
- Custom Trend metrics: each scenario writes to its own duration metric.
- Staggered start: validation and 404 probes start at
0s; load and statement scenarios start at10s. - Balance validation: debit and statement checks assert that balances do not exceed the negative limit.
- Dual output mode:
prodis a quiet k6 CLI run for CI logs;devstreams to InfluxDB for Grafana dashboards.
Getting Started
Prerequisites
- Docker
Use the published image
For most backend implementations, pull the shared GHCR image and run it against your API gateway:
docker pull ghcr.io/jonathanperis/rinha2-back-end-k6:latest
docker run --rm \
-e MODE=prod \
-e BASE_URL=http://api:9999 \
ghcr.io/jonathanperis/rinha2-back-end-k6:latest
Build locally
Build locally when changing this repository or testing an unpublished image:
docker build -t rinha-k6 .
docker run --rm -e MODE=prod -e BASE_URL=http://api:9999 rinha-k6
Dev mode with InfluxDB
Dev mode is the default when MODE is empty, and it requires an InfluxDB output target:
docker run --rm \
-e MODE=dev \
-e BASE_URL=http://api:9999 \
-e K6_INFLUXDB_ADDR=http://influxdb:8086 \
ghcr.io/jonathanperis/rinha2-back-end-k6:latest
Environment Variables
| Variable | Default | Purpose |
|---|---|---|
MODE |
dev |
Execution mode (dev or prod) |
BASE_URL |
http://localhost:9999 |
Target API endpoint |
K6_INFLUXDB_ADDR |
— | InfluxDB address (dev mode) |
Modes
- dev (default): Exports metrics to InfluxDB for real-time monitoring in Grafana dashboards
- prod: Runs
k6 run rinha-test.js --quietfor a quieter CI/log path. The entrypoint does not write an HTML artifact by itself.
Run with a backend
Sibling backend implementations can include this image as their k6 service in docker-compose.yml. Point BASE_URL at the service name and port used by the API gateway or load balancer:
services:
k6:
image: ghcr.io/jonathanperis/rinha2-back-end-k6:latest
environment:
MODE: prod
BASE_URL: http://nginx:9999
depends_on:
- nginx
Start the backend stack, then run the k6 service using that implementation’s Compose workflow.
Test Scenarios
The suite defines 5 source-backed k6 scenarios in test/stress-test/rinha-test.js. They all target BASE_URL, which defaults to http://localhost:9999.
Scenario overview
| Scenario | Executor | VUs / stages | Start | Purpose |
|---|---|---|---|---|
validacoes |
per-vu-iterations |
5 VUs, 1 iteration each | 0s |
Baseline statement, credit, debit, recent-transaction order, and invalid-request checks |
cliente_nao_encontrado |
per-vu-iterations |
1 VU, 1 iteration | 0s |
GET /clientes/6/extrato must return 404 |
debitos |
ramping-vus |
1 to 220 VUs over 2m, then hold 220 for 2m | 10s |
High-concurrency debit transactions with overdraft validation |
creditos |
ramping-vus |
1 to 110 VUs over 2m, then hold 110 for 2m | 10s |
High-concurrency credit transactions |
extratos |
per-vu-iterations |
10 VUs, 1 iteration each | 10s |
Statement reads and balance-limit consistency |
validacoes
Runs one virtual user per configured client in saldosIniciaisClientes. Each VU checks the full client workflow:
GET /clientes/{id}/extrato, expecting status200, the configured limit, and initial balance0.POST /clientes/{id}/transacoeswith credit{ valor: 1, tipo: 'c', descricao: 'toma' }.POST /clientes/{id}/transacoeswith debit{ valor: 1, tipo: 'd', descricao: 'devolve' }.GET /clientes/{id}/extrato, expecting recent transactions in the debit-then-credit order.- Invalid transaction requests, expecting
422or400depending on implementation behavior.
Invalid request cases:
| Payload issue | Expected status |
|---|---|
Decimal valor |
422 or 400 |
Invalid tipo |
422 or 400 |
| Description longer than 10 characters | 422 or 400 |
| Empty description | 422 or 400 |
null description |
422 or 400 |
cliente_nao_encontrado
Runs a single statement request against client 6:
GET /clientes/6/extrato
The expected result is 404. This keeps missing-client behavior visible as a separate metric instead of hiding it inside the general validation flow.
debitos
The debit workload ramps from 1 to 220 VUs, holds that target, and posts random debit transactions to clients 1 through 5.
stages: [
{ duration: '2m', target: 220 },
{ duration: '2m', target: 220 },
]
Accepted response statuses are 200 or 422, because debit operations can be rejected when a client would exceed the overdraft limit. Successful debit responses are checked with:
saldo >= limite * -1
creditos
The credit workload ramps from 1 to 110 VUs, holds that target, and posts random credit transactions to clients 1 through 5.
stages: [
{ duration: '2m', target: 110 },
{ duration: '2m', target: 110 },
]
Credits are expected to return 200 and preserve the same balance-limit consistency contract.
extratos
Runs 10 VUs, one iteration each, against:
GET /clientes/{id}/extrato
The response must be 200, and saldo.total must still respect the configured limit:
saldo.total >= saldo.limite * -1
Source-backed constants
The current script uses these fixed helper values:
| Constant | Current value | Source |
|---|---|---|
| Random client IDs | 1 through 5 |
randomClienteId() |
| Random transaction value | integer 1 through 10000 |
randomValorTransacao() |
| Random generated description | 10 alphanumeric characters | randomDescricao() |
| Validation delay | sleep(1) before the second statement check |
validacoes() |
Custom Trend metrics
The script exports five Trend metrics. These names are the canonical labels to use in reports and dashboards:
| Metric | Scenario |
|---|---|
debitos_duration |
debitos |
creditos_duration |
creditos |
extratos_duration |
extratos |
validacoes_duration |
validacoes |
cliente_nao_encontrado_duration |
cliente_nao_encontrado |
Each request path adds its observed duration to the scenario-specific Trend so Grafana dashboards and k6 summaries can separate bottlenecks by operation type.
Configuration
The suite is configured through environment variables and the exported options object in test/stress-test/rinha-test.js. Keep operational values in code, not in the docs, then use this page as the readable map.
Environment variables
| Variable | Default / required value | Description |
|---|---|---|
BASE_URL |
http://localhost:9999 |
Base URL for the API under test, usually the NGINX/load-balancer endpoint. |
MODE |
empty value behaves like dev in run-test.sh |
Run mode selector. Use prod for quiet CI/log runs or dev for InfluxDB export. |
K6_INFLUXDB_ADDR |
required by the xk6 output when using dev mode | InfluxDB endpoint, for example http://influxdb:8086. |
Docker run examples
Build the local image:
docker build -t rinha-k6 .
Run against an API endpoint in production/CI mode:
docker run --rm \
-e MODE=prod \
-e BASE_URL=http://api:9999 \
rinha-k6
Run with InfluxDB export in development mode:
docker run --rm \
-e MODE=dev \
-e BASE_URL=http://api:9999 \
-e K6_INFLUXDB_ADDR=http://influxdb:8086 \
rinha-k6
Direct k6 invocation
If you have the custom k6 binary available locally, pass variables with -e:
k6 run \
-e BASE_URL=http://localhost:9999 \
test/stress-test/rinha-test.js
For dev output, include the xk6 InfluxDB output:
k6 run \
-e MODE=dev \
-e BASE_URL=http://localhost:9999 \
-e K6_INFLUXDB_ADDR=http://localhost:8086 \
-o xk6-influxdb \
test/stress-test/rinha-test.js
SharedArray client data
rinha-test.js defines five clients and their credit limits in cents. k6 loads this data through SharedArray so VUs share one read-only copy:
| Client | Limit |
|---|---|
1 |
100000 |
2 |
80000 |
3 |
1000000 |
4 |
10000000 |
5 |
500000 |
Those values drive the validation scenario and the balance-limit checks after debit and statement requests.
Thresholds
The current script does not define hard k6 thresholds. It measures and reports behavior; sibling implementations or CI jobs can decide whether to fail a pipeline based on their own policy.
If thresholds are added later, keep the metric names aligned with the exported Trend metrics:
export const options = {
scenarios: { /* ... */ },
thresholds: {
debitos_duration: ['p(95)<500'],
creditos_duration: ['p(95)<500'],
extratos_duration: ['p(95)<200'],
http_req_failed: ['rate<0.01'],
},
};
Entrypoint behavior
run-test.sh waits 15 seconds before starting k6 so a backend stack launched in the same Compose project can finish booting. It accepts only three effective states:
MODE value |
Command |
|---|---|
prod |
k6 run rinha-test.js --quiet |
dev |
k6 run rinha-test.js -o xk6-influxdb |
| empty | same as dev |
Any other value exits with Invalid MODE specified. Set MODE=dev or MODE=prod.
Resource boundaries
The Rinha challenge constrains the backend services, not the k6 runner. Treat k6 as the external pressure source. The API stack under test is responsible for staying inside the challenge budget.
Run Modes
test/stress-test/run-test.sh selects the k6 output path from the MODE environment variable. The scenario profile is the same in both modes; only the output destination changes.
Use in CI, release jobs, and repeatable local checks where lower-noise k6 logs matter more than live telemetry.
Use while tuning an implementation and watching latency or throughput in Grafana.
prod: quiet CI/log run
Set MODE=prod to run k6 quietly through the bundled entrypoint:
docker run --rm \
-e MODE=prod \
-e BASE_URL=http://api:9999 \
rinha-k6
The entrypoint executes:
k6 run rinha-test.js --quiet
Use this mode when the caller is collecting stdout or CI logs outside the container. The current entrypoint does not create an HTML report file; it only passes --quiet to k6.
dev: InfluxDB export
Set MODE=dev, or leave MODE empty, to stream k6 metrics through the xk6-output-influxdb extension:
docker run --rm \
-e MODE=dev \
-e BASE_URL=http://api:9999 \
-e K6_INFLUXDB_ADDR=http://influxdb:8086 \
rinha-k6
The entrypoint executes:
k6 run rinha-test.js -o xk6-influxdb
Use this mode when an InfluxDB target is available and you want Grafana-style feedback during a tuning loop.
Mode behavior
MODE value |
Behavior |
|---|---|
prod |
Runs k6 run rinha-test.js --quiet. |
dev |
Runs k6 run rinha-test.js -o xk6-influxdb. |
| empty | Treated as dev mode by run-test.sh. |
| anything else | Fails fast with Invalid MODE specified. Set MODE=dev or MODE=prod. |
Startup delay
The entrypoint prints Tests will start in 15 seconds... and sleeps before launching k6. That delay gives the API stack under test time to finish booting when k6 is started alongside other services.
Docker image shape
The Dockerfile uses two stages:
golang:1.25-alpine3.21builds a custom k6 binary withgithub.com/grafana/xk6-output-influxdb.alpine:3.23copies the binary,rinha-test.js, andrun-test.shinto/app.
The Dockerfile creates /reports, but the current script does not write report files there. The image entrypoint is:
/app/run-test.sh
Published image:
ghcr.io/jonathanperis/rinha2-back-end-k6:latest
Supported platforms are published by the release workflow as linux/amd64 and linux/arm64/v8.
CI/CD
The repository uses GitHub Actions for the k6 image, the GitHub Pages documentation site, CodeQL analysis, and docs/source drift checks.
Workflow map
| Workflow | Trigger | Purpose |
|---|---|---|
main-release.yml |
Push to main, manual dispatch |
Build and publish the multi-platform k6 image to GHCR. |
deploy.yml |
Push to main, manual dispatch |
Call the shared Pages deploy workflow for the Astro docs site. |
codeql.yml |
Push, pull request, weekly schedule | Run JavaScript CodeQL analysis. |
docs-drift.yml |
Push and pull request paths that touch code/docs facts | Verify README, agent notes, and Pages docs against source-backed k6/workflow facts. |
Release workflow
main-release.yml builds the Docker image and publishes it to GitHub Container Registry. The current workflow publishes this tag:
ghcr.io/jonathanperis/rinha2-back-end-k6:latest
Sibling backend implementations can pull the same image so comparisons use the same stress-test profile. No SHA tag is configured in main-release.yml today. The workflow publishes linux/amd64 and linux/arm64/v8 platforms.
Pages deploy workflow
deploy.yml delegates publishing to the shared workflow:
jobs:
deploy:
uses: jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main
secrets: inherit
with:
package-manager: bun
That workflow builds the Astro 7 static site under docs/ with Bun and publishes it to GitHub Pages after changes land on main. Astro 7’s Rust compiler, default Rust/Sätteri Markdown pipeline, Vite 8/Rolldown bundling, and queued rendering are adopted by the framework upgrade. Route caching, CDN cache providers, and src/fetch.ts advanced routing are not configured because this repository publishes static Pages output rather than an SSR/edge runtime.
Public documentation route:
https://jonathanperis.github.io/rinha2-back-end-k6/docs/
Docs drift check
docs-drift.yml runs python3 scripts/check_docs_source_drift.py. The script checks the documented scenario names, Trend metrics, run modes, Docker image tag, release platforms, and homepage metric copy against the current source files.
CodeQL
codeql.yml scans the JavaScript test code on pull requests, pushes to main, and a weekly Monday 03:00 UTC schedule. Treat CodeQL failures as blocking unless the finding is understood and explicitly waived.
Branch protection
The repository is configured for rebase merges and required status checks on main. Operationally, changes should go through pull requests before landing on main, then GitHub Pages deploys from the updated branch.
Deployment checklist
Before merging docs or test-profile changes:
- Build locally from
docs/with Bun. - Smoke-check
/docs/and the section routes:/docs/getting-started/,/docs/configuration/,/docs/run-modes/,/docs/test-scenarios/, and/docs/ci-cd/. - Confirm Docs Drift, CodeQL, and review-bot status are clean.
- Merge through the PR.
- Watch the Pages deploy run for the merge commit.
- Fetch the live
/docs/route and verify a distinctive updated phrase.