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

PageDescription
ChallengeWhat Rinha de Backend 2024/Q1 requires
ArchitectureCurrent stack, services, resource constraints, and runtime flow
Getting StartedPrerequisites, run commands, endpoint smoke tests
PerformanceHow to read benchmark/report artifacts without hard-coding stale numbers
CI/CD PipelineGitHub 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:

EndpointMethodDescription
/clientes/{id}/transacoesPOSTSubmit a debit or credit transaction for a client (IDs 1-5)
/clientes/{id}/extratoGETGet 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

ServiceRoleCPURAM
webapi1-rustActix-web 4 API instance using SQLx 0.8 + Tokio0.4100MB
webapi2-rustActix-web 4 API instance using SQLx 0.8 + Tokio0.4100MB
nginxReverse proxy / load balancer on port 9999 (least_conn)0.220MB
dbPostgreSQL 16.7 Alpine with stored procedures0.5330MB
k6Shared load-test runner (MODE=dev in dev compose, MODE=prod in prod compose)not countednot counted
prometheus, grafana, influxdb, postgres-exporterDevelopment observability stacknot countednot 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), 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.
  • 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

EndpointMethodStatus CodesDescription
/clientes/{id}/transacoesPOST200, 404, 422Submit a debit or credit transaction for client IDs 1-5
/clientes/{id}/extratoGET200, 404Get current balance, credit limit, timestamp, and up to 10 recent transactions
/healthzGET200Health 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.

ServiceCPURAM
webapi1-rust0.4100MB
webapi2-rust0.4100MB
db0.5330MB
nginx0.220MB
Total1.5550MB

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.
  • 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 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.