rinha2-back-end-k6

Home

rinha2-back-end-k6 is the shared Grafana k6 stress test suite for the Rinha de Backend 2024/Q1 challenge. It validates correctness and measures throughput for all API implementations under extreme concurrency.

What is Rinha de Backend?

Rinha de Backend is a Brazilian backend engineering challenge where participants build a fictional banking API that handles concurrent credit/debit transactions with strict resource constraints: 1.5 CPU and 550MB RAM total across all services.

This k6 suite is the single source of truth for stress testing — shared across all sibling implementations (Rust, Go, .NET, Python).

Tech Stack

TechnologyPurpose
Grafana k6Load testing engine
xk6Custom k6 build with InfluxDB output extension
JavaScript (ES2015+)Test script language
Docker / Alpine 3.23Container runtime
InfluxDBTime-series metrics storage (dev mode)
GrafanaReal-time dashboard for metrics

Repository Structure

rinha2-back-end-k6/
├── test/stress-test/
│   ├── rinha-test.js       # Main test file (~318 lines, 5 scenarios)
│   └── run-test.sh         # Helper script to invoke k6
├── Dockerfile              # Multi-stage: Go 1.25 + xk6 → Alpine 3.23
├── docker-compose.yml      # Dev stack (InfluxDB + Grafana)
└── .github/workflows/
    ├── main-release.yml    # Build + push multi-platform Docker image
    ├── deploy.yml          # GitHub Pages deployment
    └── codeql.yml          # Security analysis

Docker Image

The test suite is published as a multi-platform Docker image (amd64 and arm64/v8) to the GitHub Container Registry:

ghcr.io/jonathanperis/rinha2-back-end-k6:latest

Sibling repos pull this image in their docker-compose.yml to run the full stress test:

docker compose up k6 --build --force-recreate

Key Design Patterns

  • SharedArray — client data loaded once, shared across all VUs for efficiency
  • Custom Trend metrics — 5 named Trend metrics track latency per endpoint type
  • Staggered start — validation scenarios run at 0s; load scenarios ramp from 10s
  • Balance validation — checks saldo >= limite * -1 after each debit
  • Dual output modeprod generates an HTML report; dev streams to InfluxDB

Test Scenarios

The test suite defines 5 scenarios that run sequentially and in parallel, covering both correctness validation and high-throughput load generation. All scenarios target the same BASE_URL (default: http://localhost:9999).

Scenario Overview

ScenarioVUsIterations / DurationStart TimePurpose
validacoes51 iter each0sEdge-case validation (invalid inputs, 404, 422)
cliente_nao_encontrado11 iter0sValidates 404 for unknown client IDs
debitos1 → 2204 min ramp10sHigh-concurrency debit transactions
creditos1 → 1104 min ramp10sHigh-concurrency credit transactions
extratos101 iter eachAfter loadFinal balance/statement consistency check

validacoes

Runs 5 VUs concurrently, each executing 1 iteration of edge-case checks:

  • Debit exceeding client limit → expects 422
  • Invalid transaction type → expects 422
  • Missing description field → expects 422
  • Description too long (>10 chars) → expects 422
  • Empty description → expects 422

cliente_nao_encontrado

Single VU, single iteration. Sends requests to client IDs 0, 6, and 999 — all of which must return 404 Not Found.

debitos

The primary load scenario. Ramps from 1 to 220 VUs over 4 minutes, continuously posting debit transactions to randomly selected clients (IDs 1–5). After each debit, the response body is validated:

// Balance must never go below the negative of the limit
check(res, {
  'debit balance valid': (r) => r.json().saldo >= r.json().limite * -1,
});

creditos

Parallel to debitos. Ramps from 1 to 110 VUs over 4 minutes, posting credit transactions to randomly selected clients. Credits cannot fail due to insufficient funds, so this scenario validates basic HTTP correctness (200) and response shape.

extratos

Final consistency check. After the load scenarios complete, 10 VUs each request the account statement (GET /clientes/{id}/extrato) for one client. Validates that:

  • Response is 200 OK
  • saldo.total is a number
  • ultimas_transacoes is an array
  • Balance is consistent with the limit (saldo.total >= saldo.limite * -1)

Custom Trend Metrics

The test file defines 5 custom Trend metrics to track p95/p99 latency broken down by operation type:

const transacaoTrend    = new Trend('transacao_duration');
const extratoTrend      = new Trend('extrato_duration');
const validacaoTrend    = new Trend('validacao_duration');
const notFoundTrend     = new Trend('not_found_duration');
const consistencyTrend  = new Trend('consistency_duration');

These appear as separate metrics in Grafana and the HTML report, making it easy to identify which operation type is the bottleneck.

Configuration

The test suite is configured entirely through environment variables, making it easy to switch between local development and CI/CD production runs without modifying any test code.

Environment Variables

VariableDefaultDescription
MODEprodRun mode: prod (HTML report) or dev (InfluxDB export)
BASE_URLhttp://localhost:9999Base URL of the API under test (NGINX load balancer endpoint)
K6_INFLUXDB_ADDRInfluxDB address for dev mode (e.g. http://influxdb:8086)

Passing Variables

When running directly with k6:

k6 run \
  -e MODE=dev \
  -e BASE_URL=http://localhost:9999 \
  -e K6_INFLUXDB_ADDR=http://localhost:8086 \
  test/stress-test/rinha-test.js

When running via Docker Compose:

docker compose up k6 --build --force-recreate

The docker-compose.yml passes environment variables to the k6 container automatically. Override them with a .env file or inline exports.

SharedArray — Client Data

Client data (IDs 1–5, with their credit limits) is loaded once at startup using k6’s SharedArray to avoid redundant memory allocation across VUs:

import { SharedArray } from 'k6/data';

const clients = new SharedArray('clients', function () {
  return [
    { id: 1, limite: 100000 },
    { id: 2, limite: 80000  },
    { id: 3, limite: 1000000 },
    { id: 4, limite: 10000000 },
    { id: 5, limite: 500000 },
  ];
});

Thresholds

The suite does not enforce hard thresholds by default — it is designed to measure and report, not pass/fail CI. Each sibling implementation’s own CI pipeline decides whether to treat threshold violations as failures.

Custom thresholds can be added to the options export in rinha-test.js:

export const options = {
  scenarios: { /* ... */ },
  thresholds: {
    'transacao_duration': ['p(95)<500'],
    'extrato_duration':   ['p(95)<200'],
    http_req_failed:      ['rate<0.01'],
  },
};

Resource Constraints

The k6 container itself is intentionally kept outside the 1.5 CPU / 550MB RAM budget. Only the API services (2x webapi + NGINX + PostgreSQL) are constrained.

Run Modes

The suite supports two output modes controlled by the MODE environment variable. Both modes run the same 5 scenarios — only the output destination differs.

prod — HTML Report

The default mode. k6 runs the full test suite and generates a self-contained HTML report at the end. This is used in CI/CD workflows where the report is archived as a build artifact or published to GitHub Pages.

# Run with prod mode (default)
docker compose up k6 --build --force-recreate

# Or directly:
k6 run -e MODE=prod test/stress-test/rinha-test.js

The HTML report includes:

  • Request rate, VU count, and iteration timeline
  • HTTP response time percentiles (p50, p90, p95, p99)
  • Per-scenario breakdown
  • Custom Trend metric charts (all 5 named metrics)
  • Check pass/fail counts

dev — InfluxDB + Grafana

Development mode streams all metrics in real time to InfluxDB, which is visualised in Grafana via a pre-configured dashboard. This is ideal for iterative tuning — you can watch latency graphs live as you adjust the API under test.

# Start the full dev stack (k6 + InfluxDB + Grafana)
MODE=dev docker compose up --build

# Or pass variables explicitly:
k6 run \
  -e MODE=dev \
  -e K6_INFLUXDB_ADDR=http://localhost:8086 \
  test/stress-test/rinha-test.js

Dev Stack Services

ServicePortPurpose
k6Runs test scenarios, streams metrics to InfluxDB
InfluxDB8086Time-series storage for k6 metrics
Grafana3000Real-time dashboards (k6 + system metrics)

run-test.sh

A convenience shell script at test/stress-test/run-test.sh wraps the k6 run invocation for MODE handling:

./test/stress-test/run-test.sh

Docker Image

The Dockerfile uses a multi-stage build:

  1. Stage 1golang:1.25-alpine: builds xk6 with the InfluxDB output extension
  2. Stage 2alpine:3.23: copies the compiled k6 binary and test scripts

The final image is minimal (~30MB) and published as:

ghcr.io/jonathanperis/rinha2-back-end-k6:latest

Supported platforms: linux/amd64, linux/arm64/v8.

CI/CD

The repository uses three GitHub Actions workflows covering Docker image release, GitHub Pages deployment, and security analysis.

Workflows

WorkflowTriggerPurpose
main-release.ymlPush to mainBuild + push multi-platform Docker image to GHCR
deploy.ymlPush to mainBuild Astro docs site and deploy to GitHub Pages
codeql.ymlPush / PR / weeklyCodeQL security and quality analysis

main-release.yml

Builds the xk6 Docker image for both linux/amd64 and linux/arm64/v8 using docker buildx and pushes to the GitHub Container Registry:

ghcr.io/jonathanperis/rinha2-back-end-k6:latest
ghcr.io/jonathanperis/rinha2-back-end-k6:{sha}

Sibling repos (Rust, Go, .NET, Python) pull this image in their own CI/CD pipelines to run the full stress test after each release.

deploy.yml

Builds the Astro docs site with bun install && bun run build and uploads the docs/out/ directory to GitHub Pages:

- uses: actions/setup-node@v6
  with:
    node-version: '22'
    cache: 'npm'
    cache-dependency-path: docs/package-lock.json

- name: Install dependencies
  run: bun install
  working-directory: docs

- name: Build
  run: bun run build
  working-directory: docs
  env:
    PUBLIC_GA_ID: ${{ secrets.PUBLIC_GA_ID }}

- uses: actions/upload-pages-artifact@v4
  with:
    path: docs/out/

codeql.yml

Runs GitHub CodeQL analysis on every push to main, on every pull request, and on a weekly schedule. It scans the JavaScript test code for security vulnerabilities and code quality issues.

Branch Protection

All changes to main must go through a pull request. Direct pushes to main are disabled. PRs use rebase merge only (squash and merge commits are disabled).

Docker Image Registry

The published image is publicly available at:

docker pull ghcr.io/jonathanperis/rinha2-back-end-k6:latest

No authentication is required to pull. The image is rebuilt and re-tagged on every push to main.