An entry for Rinha de Backend 2026, written in Odin. The challenge: a fraud-detection API that turns each transaction into a 14-dimensional vector and answers with an exact 5-nearest-neighbour vote over a fixed set of 3,000,000 labelled reference vectors — all within 1.0 CPU / 350 MB total, on a Mac Mini Late 2014 (Intel Haswell).
POST /fraud-score → vectorize → quantize → exact 5-NN → {"approved", "fraud_score"}
GET /ready → 200 once warm
The whole thing is built around two ideas: precompute everything that never changes, and keep the per-request hot path branch-light and SIMD-friendly.
- Vectorization (
src/vector.odin) followsDETECTION_RULES.mdexactly, including the-1sentinels for a missing previous transaction. Every value is quantized×10000intoi16. The reference dataset ships with 4-decimal precision, so integer math at this scale is lossless and reproduces the exact distance ordering used to generate the truth labels. - The index (
src/model.odin) is a single balanced KD-tree with a 16-dim bounding box per node, built offline bytools/preprocessand serialized to a flat, mmap-friendly file (~126 MB) that is baked into the image. Splits are taken on the widest dimension, so the large-range binary dims (is_online, card_present, unknown_merchant, history sentinels) are split first — naturally separating the data the way an explicit bucket partition would. - Search (
src/classifier.odin) is exact 5-NN: a nearer-child-first descent that prunes any subtree whose box lower bound can't beat the current 5th-nearest. Distances are squared Euclidean overi16lanes accumulated ini64, written to auto-vectorize under-microarch:haswell. - HTTP (
src/server.odin) is a hand-rolled single-threadedepollserver (HTTP/1.1 keepalive). The JSON body is parsed in place by a fixed-schema parser (src/payload.odin) — no allocation on the hot path. There are only 6 possible answers (fraud count 0–5), so the full HTTP responses are precomputed at startup and the handler just writes the right one. - Deployment: nginx round-robin in front of two identical Odin processes, each
pinned to a fraction of the CPU (see
docker-compose.yml).
| Metric | Value |
|---|---|
| Detection vs. contest truth (54,100 entries) | 0 FP, 0 FN, 0 errors → score_det = 3000 (max) |
| Search latency (microbench, pessimistic random queries) | mean ~170 µs, p99 ~710 µs (single thread) |
| Model size | ~126 MB (fits the 155 MB per-instance budget) |
| Per-instance RSS | ~127 MB |
In-distribution traffic prunes far better than the random-query microbench, so real latency is lower. Detection is exact, so the score is bounded only by p99.
src/ library package `otto` (logic + tests)
vector.odin 14-dim vectorization, quantization, MCC/normalization constants
timeparse.odin ISO-8601 → epoch / hour / day-of-week
payload.odin fixed-schema, allocation-free JSON request parser
model.odin KD-tree build + binary model format + reference-JSON parser
classifier.odin exact 5-NN search + brute-force oracle (for tests)
server.odin epoll HTTP/1.1 server, precomputed responses
*_test.odin unit + property tests
cmd/server/ the API executable (imports `otto`)
tools/
preprocess/ references.json → model.bin (build time)
bench/ search-latency microbenchmark
verify/ accuracy gate vs test-data.json
Dockerfile, docker-compose.yml, nginx.conf
.github/workflows/ ci.yml (test+build), image.yml (build+push GHCR)
Requires the Odin compiler (pacman -S odin on Arch/Manjaro, or nix-shell).
make test # unit + property tests (search == brute-force oracle)
make data # fetch references.json.gz (from the local harness or upstream)
make verify # build the model, check 0 FP/FN against the contest truth
make bench # search-latency microbenchmark
make up # build + run the full stack (nginx + 2 api) via docker compose
make k6 # run the contest k6 load test against :9999
make bench-vm # run the whole load test inside an isolated banger microVMThe reference dataset (resources/references.json.gz, ~50 MB) is not vendored;
make data fetches it. The upstream contest repo lives in rinha-de-backend-2026/
(git-ignored) and is used as the local benchmark harness.
make bench-vm builds and load-tests the stack inside a throwaway
banger Firecracker microVM (2 vCPU / 8 GB, mimicking the
grading Mac Mini) so nothing touches host Docker, ports, or network state, and
tears it down afterwards (KEEP=1 keeps it). Measured there:
p99 1.93 ms, 0 FP / 0 FN / 0 errors, final_score 5715 / 6000 under the
enforced 1.0 CPU / 350 MB budget.
Source lives on main. The submission branch carries only docker-compose.yml,
nginx.conf, and info.json at the root, referencing the public GHCR image built
by the image workflow. The load balancer listens on 9999.
Detection is already maxed; future work targets p99 only: per-query thread
fan-out to cut head-of-line blocking, a hand-tuned SIMD distance kernel if
auto-vectorization underperforms, and a zero-copy SCM_RIGHTS load balancer in
place of nginx if the LB ever shows up in the budget.