rinha2-back-end-rust

rinha2-back-end-rust

Rust/Actix-web 4 implementation for the Rinha de Backend 2024/Q1 challenge. It manages a fictional bank API with transaction processing and balance statements under strict resource constraints (1.5 CPU, 550MB RAM total across the challenge containers).

Wiki Pages

Page Description
Challenge What Rinha de Backend 2024/Q1 requires
Architecture Current stack, services, resource constraints, and runtime flow
Getting Started Prerequisites, run commands, endpoint smoke tests
Performance How to read benchmark/report artifacts without hard-coding stale numbers
CI/CD Pipeline GitHub Actions workflows and release/deploy path

Key Features

  • Single Rust API entrypoint (src/WebApi/main.rs, 173 total lines at this revision)
  • Actix-web 4 with Tokio async runtime and SQLx 0.8
  • Rust 2024 package; Docker builder currently uses rust:1.95
  • PostgreSQL stored procedures for balance updates and statement aggregation
  • Health endpoint at /healthz returns Healthy
  • Source-backed drift guard: python3 scripts/check_docs_drift.py

GitHub · Jonathan Peris

Challenge

Rinha de Backend 2024/Q1

The Rinha de Backend is a Brazilian backend programming challenge. The 2024/Q1 edition simulates a fictional bank called “Rinha Financeira” that manages up to 5 named clients, each seeded at startup with a credit limit and initial balance.

Endpoints

Two API endpoints are required:

Endpoint Method Description
/clientes/{id}/transacoes POST Submit a debit or credit transaction for a client (IDs 1-5)
/clientes/{id}/extrato GET Get a client’s current balance, credit limit, and recent transactions

Constraints

The challenge imposes strict resource limits across all containers combined:

  • 1.5 CPU total shared across all services
  • 550MB RAM total shared across all services
  • The system is stress tested using Grafana k6 with concurrent users submitting transactions and querying statements

Source

Full specification: github.com/zanfranceschi/rinha-de-backend-2024-q1

Architecture

Overview

The system follows the Rinha de Backend topology used by this repository: two Rust/Actix-web API containers behind NGINX, one PostgreSQL database, and optional observability/load-test services in the development compose file.

Services

Service Role CPU RAM
webapi1-rust Actix-web 4 API instance using SQLx 0.8 + Tokio 0.4 100MB
webapi2-rust Actix-web 4 API instance using SQLx 0.8 + Tokio 0.4 100MB
nginx Reverse proxy / load balancer on port 9999 (least_conn) 0.2 20MB
db PostgreSQL 16.7 Alpine with stored procedures 0.5 330MB
k6 Shared load-test runner (MODE=dev in dev compose, MODE=prod in prod compose) not counted not counted
prometheus, grafana, influxdb, postgres-exporter Development observability stack not counted not counted

The compose CPU/RAM limits for the challenge services total 1.5 CPU and 550MB.

Load Balancing

NGINX listens on :9999 and proxies all routes to an upstream named api with least_conn across webapi1-rust:8080 and webapi2-rust:8080. The dev compose file also maps the two API containers to host ports 6968 and 6969, but challenge-compatible traffic should go through NGINX.

API Runtime

The API is a single Rust entrypoint (src/WebApi/main.rs, 173 total lines at this revision):

  • GET /clientes/{id}/extrato validates the client ID against a lazy static HashMap, then calls GetSaldoClienteById($1).
  • POST /clientes/{id}/transacoes validates the client ID and payload (tipo, descricao, valor), accepts PascalCase JSON aliases (Valor, Tipo, Descricao), then calls InsertTransacao($1, $2, $3, $4).
  • GET /healthz returns Healthy for compose and CI smoke checks.
  • The SQLx pool is created from DATABASE_URL with max_connections(5) per API instance.

Database

Business logic is implemented in PostgreSQL stored procedures over UNLOGGED tables:

  • Clientes stores the five seeded clients and their current balance (SaldoInicial).
  • Transacoes stores accepted transactions and has an index on (ClienteId, Id DESC) for recent-statement reads.
  • InsertTransacao applies credit/debit balance updates and inserts the transaction only when the update succeeds. If a debit exceeds the available limit, the function returns the unchanged current balance without inserting a transaction row; the Rust handler serializes that as a 200 response today.
  • GetSaldoClienteById returns the current balance, limit, timestamp, and up to 10 latest transactions as JSONB.

The database command is tuned for benchmark throughput, not durability:

  • synchronous_commit=0 — no wait for WAL flush
  • fsync=0 — skip fsync on writes
  • full_page_writes=0 — skip full page writes

Container Images

The API Dockerfile builds with rust:1.95 and runs on debian:bookworm-slim as a non-root app user. The release workflow publishes ghcr.io/jonathanperis/rinha2-back-end-rust:latest as the multi-arch image used by prod/docker-compose.yml.

Getting Started

Prerequisites

  • Docker with Docker Compose

Clone and Run

git clone https://github.com/jonathanperis/rinha2-back-end-rust.git
cd rinha2-back-end-rust
docker compose up nginx -d --build

Access

The challenge-facing API is available at http://localhost:9999 through NGINX.

The dev compose file also maps the two API containers directly:

  • http://localhost:6968webapi1-rust:8080
  • http://localhost:6969webapi2-rust:8080

Use port 9999 for normal validation and load-test requests.

Endpoints

Endpoint Method Status Codes Description
/clientes/{id}/transacoes POST 200, 404, 422 Submit a debit or credit transaction for client IDs 1-5
/clientes/{id}/extrato GET 200, 404 Get current balance, credit limit, timestamp, and up to 10 recent transactions
/healthz GET 200 Health check returning Healthy

Transaction payloads must have tipo equal to c or d, a non-empty descricao of at most 10 characters, and a positive integer valor. The DTO also accepts PascalCase aliases (Valor, Tipo, Descricao) in addition to the lowercase challenge field names (valor, tipo, descricao).

Current Response Contract

POST /clientes/{id}/transacoes returns:

{
  "id": 1,
  "limite": 100000,
  "saldo": 1000
}

GET /clientes/{id}/extrato returns a saldo object with total, limite, and data_extrato, plus ultimas_transacoes entries serialized as valor, tipo, and descricao.

The current stored procedure handles a debit that would exceed the client limit by leaving the balance unchanged and skipping the transaction insert, then returning the current balance to the API. That path currently responds with 200; 422 is reserved for invalid payloads or database errors.

Seeded client limits:

ID Limit
1 100000
2 80000
3 1000000
4 10000000
5 500000

Example Requests

Health Check

curl http://localhost:9999/healthz

Create Transaction

curl -X POST http://localhost:9999/clientes/1/transacoes   -H "Content-Type: application/json"   -d '{"valor": 1000, "tipo": "c", "descricao": "deposito"}'

Get Statement

curl http://localhost:9999/clientes/1/extrato

Stress Tests

cp .env.example .env
docker compose up k6 --build --force-recreate

In docker-compose.yml, k6 runs in MODE=dev and exports time-series data to InfluxDB/Grafana. Copy .env.example first so INFLUXDB_PASSWORD and INFLUXDB_TOKEN are defined for InfluxDB, Grafana, and xk6 output. The release workflow uses prod/docker-compose.yml, where k6 runs in MODE=prod and writes an HTML stress-test report artifact.

Performance

Resource Constraints

The challenge allows a total of 1.5 CPU and 550MB RAM across the core API, database, and load-balancer containers.

Service CPU RAM
webapi1-rust 0.4 100MB
webapi2-rust 0.4 100MB
db 0.5 330MB
nginx 0.2 20MB
Total 1.5 550MB

Observability (prometheus, grafana, influxdb, postgres-exporter) and k6 are part of the development/test harness and are not part of the challenge resource total.

Source of Truth for Results

Avoid treating fixed numbers in Markdown as the performance source of truth. This repository stores generated stress-test report artifacts under docs/public/reports/, and the main-release.yml workflow uploads the newest k6 HTML report from prod/conf/stress-test/reports/stress-test-report.html.

Use those generated reports and workflow artifacts for exact latency, throughput, and error-rate figures. This wiki page documents where the results come from and what runtime shape produced them.

Performance-Relevant Runtime Choices

  • Two API instances behind NGINX least_conn.
  • SQLx pool size is 5 connections per API instance.
  • Client existence and limits are validated from a lazy static Rust HashMap for the five seeded clients.
  • Balance mutation and statement aggregation are handled in PostgreSQL stored procedures.
  • Clientes and Transacoes are UNLOGGED tables for challenge throughput.
  • Debits that would exceed a client’s limit do not insert a transaction row; the stored procedure returns the unchanged balance.
  • PostgreSQL durability flags are disabled for benchmarking: synchronous_commit=0, fsync=0, and full_page_writes=0.

Stress Testing

Load tests are run using the shared rinha2-back-end-k6 suite, which simulates concurrent users performing debits, credits, validations, and statement queries.

Development mode:

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

Release mode is executed by GitHub Actions with prod/docker-compose.yml, MODE=prod, and HTML report artifact upload.

CI/CD Pipeline

Workflows

This repository uses four GitHub Actions workflows plus a source-backed docs drift guard.

build-check.yml

  • Trigger: Pull requests to main and manual dispatch
  • Rust build: cargo build --release --manifest-path ./src/WebApi/Cargo.toml
  • Docs drift guard: python3 scripts/check_docs_drift.py
  • Container smoke: docker compose -f ./docker-compose.yml up nginx --wait, then GET http://localhost:9999/healthz
  • Purpose: Catch Rust build failures, README/wiki drift, and compose health regressions before merging

main-release.yml

  • Trigger: Push to main and manual dispatch
  • Build/release path: build Rust release binary, push amd64 image as latest, push arm64 image as latest-arm64, merge both digests into the multi-arch latest manifest
  • Validation: start prod/docker-compose.yml, poll /healthz, then run k6 in MODE=prod
  • Artifact: upload ./prod/conf/stress-test/reports/stress-test-report.html as stress-test-report
  • Purpose: Publish production-ready container images and preserve the load-test report from the release run

codeql.yml

  • Trigger: Push to main, pull requests to main, and weekly schedule (0 3 * * 1)
  • Steps: Set up stable Rust, initialize CodeQL for Rust with security-and-quality queries, build the release binary, perform analysis
  • Purpose: Continuous security and code quality analysis

deploy.yml

  • Trigger: Push to main and manual dispatch
  • Steps: Calls the shared jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main reusable workflow with package-manager: bun
  • Purpose: Build and publish the Astro 7 docs site to GitHub Pages

Docs site toolchain

  • PR validation: install the Bun docs workspace, run ESLint, and build the Astro 7 site with NODE_ENV=production
  • Runtime floor: Node 22.12+ for Astro 7, with Node 24 used in CI before Bun setup
  • Adopted Astro 7 features: Vite 8/Rolldown bundling, the Rust .astro compiler, queued rendering defaults, and the Sätteri Markdown processor
  • Skipped Astro 7 features: route caching, CDN cache providers, and src/fetch.ts advanced routing, because this repo deploys static GitHub Pages output instead of server-rendered routes

Local Documentation Drift Check

python3 scripts/check_docs_drift.py

The guard checks README/wiki claims against the current code and configuration: line count, Rust edition, Docker builder image, endpoints, workflow triggers, image tags, compose resources, and wiki navigation coverage. It also rejects known-stale phrases such as old line-count/version/performance claims.