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
/healthzreturnsHealthy - Source-backed drift guard:
python3 scripts/check_docs_drift.py
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}/extratovalidates the client ID against a lazy staticHashMap, then callsGetSaldoClienteById($1).POST /clientes/{id}/transacoesvalidates the client ID and payload (tipo,descricao,valor), then callsInsertTransacao($1, $2, $3, $4).GET /healthzreturnsHealthyfor compose and CI smoke checks.- The SQLx pool is created from
DATABASE_URLwithmax_connections(5)per API instance.
Database
Business logic is implemented in PostgreSQL stored procedures over UNLOGGED tables:
Clientesstores the five seeded clients and their current balance (SaldoInicial).Transacoesstores accepted transactions and has an index on(ClienteId, Id DESC)for recent-statement reads.InsertTransacaoapplies credit/debit balance updates and inserts the transaction only when the update succeeds.GetSaldoClienteByIdreturns 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 flushfsync=0— skip fsync on writesfull_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:6968→webapi1-rust:8080http://localhost:6969→webapi2-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.
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
docker compose up k6 --build --force-recreate
In docker-compose.yml, k6 runs in MODE=dev and exports time-series data to InfluxDB/Grafana. 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
HashMapfor the five seeded clients. - Balance mutation and statement aggregation are handled in PostgreSQL stored procedures.
ClientesandTransacoesare UNLOGGED tables for challenge throughput.- PostgreSQL durability flags are disabled for benchmarking:
synchronous_commit=0,fsync=0, andfull_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
mainand 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, thenGET http://localhost:9999/healthz - Purpose: Catch Rust build failures, README/wiki drift, and compose health regressions before merging
main-release.yml
- Trigger: Push to
mainand manual dispatch - Build/release path: build Rust release binary, push amd64 image as
latest, push arm64 image aslatest-arm64, merge both digests into the multi-archlatestmanifest - Validation: start
prod/docker-compose.yml, poll/healthz, then run k6 inMODE=prod - Artifact: upload
./prod/conf/stress-test/reports/stress-test-report.htmlasstress-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 tomain, and weekly schedule (0 3 * * 1) - Steps: Set up stable Rust, initialize CodeQL for Rust with
security-and-qualityqueries, build the release binary, perform analysis - Purpose: Continuous security and code quality analysis
deploy.yml
- Trigger: Push to
mainand manual dispatch - Steps: Calls the shared
jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@mainreusable workflow withpackage-manager: bun - Purpose: Build and publish the Astro docs site to GitHub Pages
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.