backend / polyglot-bench

Polyglot Checkout Bench

One database-bound checkout transaction implemented ten ways in TypeScript, Go and Rust, proved output-identical, then load-tested on pinned cores with significance tests.

2026 · research · open source

orch → loadgen: spawns per run (taskset)orch → pg: reset.sh + VACUUMloadgen → ts: HTTP/1.1 loopbackloadgen → go: HTTP/1.1 loopbackloadgen → rust: HTTP/1.1 loopbackverify → go: fixed corpus replayts → pg: Bun.sql / pg wirego → pg: pgx / GORMrust → pg: tokio-postgres poolloadgen → results: run JSON + /proc samplesresults → stats: rebuild tables, figures, paperresults → dash: live file readrun.py orchestratorrun.py orchestrator (client) 450-run matrix, shuffled, reset per runverify.py conformanceverify.py conformance (client) canonical JSON diff, 10 stacksGo load generatorGo load generator (client) closed64 / open300 / open900, cores 6-7TypeScript services x3TypeScript services x3 (service) Bun-only, Hono, FastifyRust services x3Rust services x3 (service) hyper, Axum, ActixGo services x4Go services x4 (service) net/http, Fiber, Gin, Gin+GORMresults/*.jsonresults/*.json (store) 450 run files with timeseriesPostgres 18 (Docker)Postgres 18 (Docker) (store) 11 tables, cpuset 0-2stats.py + figures +paperstats.py + figures + paper (worker) Friedman, Nemenyi, HolmBun dashboardBun dashboard (service) local :7777 over results/spawns per run (tasks…reset.sh + VACUUMHTTP/1.1 loopbackHTTP/1.1 loopbackHTTP/1.1 loopbackfixed corpus replayBun.sql / pg wirepgx / GORMtokio-postgres poolrun JSON + /proc samp…rebuild tables, figur…live file read orch → loadgen: spawns per run (taskset)orch → pg: reset.sh + VACUUMloadgen → ts: HTTP/1.1 loopbackloadgen → go: HTTP/1.1 loopbackloadgen → rust: HTTP/1.1 loopbackverify → go: fixed corpus replayts → pg: Bun.sql / pg wirego → pg: pgx / GORMrust → pg: tokio-postgres poolloadgen → results: run JSON + /proc samplesresults → stats: rebuild tables, figures, paperresults → dash: live file readrun.py orchestratorrun.py orchestrator (client) 450-run matrix, shuffled, reset per runverify.pyconformanceverify.py conformance (client) canonical JSON diff, 10 stacksGo load generatorGo load generator (client) closed64 / open300 / open900, cores 6-7TypeScript servicesx3TypeScript services x3 (service) Bun-only, Hono, FastifyRust services x3Rust services x3 (service) hyper, Axum, ActixGo services x4Go services x4 (service) net/http, Fiber, Gin, Gin+GORMresults/*.jsonresults/*.json (store) 450 run files with timeseriesPostgres 18 (Docker)Postgres 18 (Docker) (store) 11 tables, cpuset 0-2stats.py + figures +paperstats.py + figures + paper (worker) Friedman, Nemenyi, HolmBun dashboardBun dashboard (service) local :7777 over results/spawns per run (tasks…reset.sh + VACUUMHTTP/1.1 loopbackHTTP/1.1 loopbackHTTP/1.1 loopbackfixed corpus replayBun.sql / pg wirepgx / GORMtokio-postgres poolrun JSON + /proc samp…rebuild tables, figur…live file read
clientservicestoreworkerasync
  • [*]LanguagesGo · Rust · TypeScript · Python · SQL
  • [*]Frameworksnet/http · Fiber · Gin · GORM · pgx · hyper · Axum · Actix Web · tokio-postgres · deadpool-postgres · Bun.serve · Bun.sql · Hono · Fastify · node-postgres · SciPy
  • [*]StoresPostgres
  • [*]AsyncGo goroutine scheduler (open-loop arrival schedule) · pgx batch pipelining
  • [*]InfraDocker · docker-compose · taskset / cpuset core pinning · Linux /proc sampling

The backend here is ten HTTP services that implement one written contract (spec/SPEC.md): POST /api/quote does six reads and runs a discount engine with stacking rules, a 60% cap per line and a coupon split pro rata with remainder cents to the largest line; POST /api/checkout runs the same engine inside one transaction that locks inventory with SELECT ... FOR UPDATE in product-id order, returns 409 on short stock, and writes the order, a multi-row line insert, reservations, two ledger entries, a coupon bump and a loyalty row; GET /api/customers/:id/summary aggregates the last 50 orders. Integer cents throughout.

The stacks are Bun with a 52-line hand-rolled router over Bun.sql, Bun + Hono and Node + Fastify over node-postgres, Go on net/http, Fiber and Gin over pgx/v5, Gin over GORM, and Rust on hyper, Axum and Actix over tokio-postgres with deadpool. Inside each language the pricing engine is one shared module, so framework rows differ only by framework. Postgres 18 holds 11 tables seeded deterministically (50,000 customers, 200,000 orders, 600,000 lines).

The harness is the larger half: a Go load generator with closed-loop and open-loop modes, where open-loop latency is measured from each request's scheduled send time to avoid coordinated omission; a Python orchestrator that resets and vacuums the database before every run, pins Postgres, service and generator to separate cores, and shuffles run order per repetition; a conformance harness that diffs canonical JSON across all ten stacks before any timing; a statement-count oracle that reads Postgres's own statement log; and a stats pipeline (Friedman, Nemenyi, Holm-corrected Mann-Whitney, bootstrap CIs) that regenerates the README, RESULTS.md and a LaTeX paper from the 450 run files.

The final suite logged 6,029,371 requests across 450 runs with zero errors. At a fixed 300 req/s the ten stacks sit within about 1 ms of p99 on quote; at saturation Rust and Go are indistinguishable (rust-axum vs go-fiber 0.99x, p=0.71), and swapping pgx for GORM under the same Gin router costs 1.84x throughput, more than the gap between languages.

How it holds up

Auth
None by design: the services are unauthenticated benchmark targets bound to loopback, so the spec measures only the transaction.
Data
11 tables in db/schema.sql (customer_tier, customer, product, inventory, discount_rule, tax_rate, coupon, order, order_item, ledger_entry, loyalty_transaction), generated from fixed seed 20260921 by db/seed.py; autovacuum is disabled on the six churn tables and bench/reset.sh deletes rows above order id 200000, zeroes reservations and coupon counters, resets sequences and runs VACUUM (ANALYZE) before every run.
Consistency
Checkout runs in one transaction that takes row locks on inventory with SELECT ... FOR UPDATE ordered by product_id in all ten stacks (consistent lock order prevents deadlocks between concurrent checkouts), checks availability as on_hand - reserved, and only then writes; in Go the reservation, ledger, coupon and loyalty writes go out as one pgx.Batch round trip before COMMIT.
Failure
Short stock returns a typed 409 insufficient_stock with the SKU and rolls back via a deferred tx.Rollback; the orchestrator catches a failed run, prints the service's last log lines and stops the service in a finally block, and each run records a fixed-CPU machine probe so runs taken on a busy box can be flagged rather than averaged in (58 of 450 flagged per README).
Testing
No unit-test files (git ls-files finds none). Verification is a cross-implementation conformance harness (bench/verify.py: 10 stacks x 150 requests x 3 endpoints by default, from a 2,000-request price corpus and 2,000-id summary corpus, diffing canonical JSON against the reference stack) plus a statement-count oracle (bench/count_statements.sh) that checks 6 statements per quote and 15 per checkout against Postgres's log_statement output.

Techniques

  1. Coordinated-omission-free open-loop loadA scheduler goroutine emits intended send times at a fixed interval and workers record latency from that intended time, not the actual send, so a server that falls behind cannot hide its queueing delay in its own slowness.bench/loadgen/main.go:7 bench/loadgen/main.go:233 bench/loadgen/main.go:251
  2. Deadlock-safe inventory lockingEvery implementation sorts product ids and takes SELECT ... FOR UPDATE in product_id order inside the checkout transaction, rejects short stock with 409 before any write, then pipelines the remaining writes in one batch before COMMIT.services/go/internal/store/store.go:234 services/go/internal/store/store.go:259 services/go/internal/store/store.go:314 services/go/internal/store/store.go:328 services/rust/common/src/db.rs:139 services/ts/bun-only/src/db/queries.ts:121
  3. Cross-implementation conformance oracleBefore timing, a fixed corpus is replayed against all ten stacks and canonicalised JSON (sorted keys, orderId dropped, timestamps normalised to instants) is diffed against the reference stack, so a stack that rounds differently fails before it can win.bench/verify.py:13 bench/verify.py:60
  4. Statement-count oracle from the database's own logTurns on log_statement=all, replays one fixed request, and counts LOG lines that reached Postgres, which is how GORM's extra Preload SELECT (7/16 vs 6/15 statements) was measured rather than inferred.bench/count_statements.sh:17 bench/count_statements.sh:21 services/go/internal/gormstore/gormstore.go:192
  5. Deterministic per-run database resetBefore every one of the 450 runs the orchestrator deletes new orders, zeroes reservations and coupon counters, resets sequences and vacuums explicitly, with autovacuum turned off on the churn tables so it cannot fire mid-measurement.bench/run.py:123 bench/reset.sh:5 bench/reset.sh:19 db/schema.sql:128
  6. Integer-cent pricing with pro-rata coupon splitDiscounts are capped at 60% of a line in basis points and the coupon is divided across lines pro rata with the remainder cents assigned to the largest line, so all ten implementations can agree to the cent.services/go/internal/domain/domain.go:193 services/go/internal/domain/domain.go:244

Counted

  • 34commits
    git rev-list --count HEAD
  • 10 (3 TypeScript, 4 Go, 3 Rust)service implementations
    bench/stacks.json
  • 450 (45 per stack: 3 workloads x 3 load profiles x 5 reps)measured runs
    ls results | grep __ | wc -l
  • 6,029,371 with 0 errorsrequests served in the final suite
    sum of requests/errors over results/*.json
  • 11Postgres tables
    grep -c 'CREATE TABLE' db/schema.sql
  • 2,000 price requests + 2,000 summary ids; 150 per endpoint replayed by defaultconformance corpus
    bench/corpus.json; bench/verify.py:9
  • Go 1,524 / Rust 804 / TypeScript 850service LOC
    git ls-files 'services/*.go|*.rs|*.ts' | xargs wc -l
  • Go load generator 498, Python 1,937, shell 229harness + analysis LOC
    git ls-files ... | xargs wc -l
  • rust-axum / go-fiber 0.99x [0.86-1.07], p=0.71Rust vs Go at saturation
    analysis/stats.json plannedContrasts
  • go-gin / go-gin-gorm 1.84x [1.54-2.56] throughput; 2.65x on checkoutORM cost (same Gin router)
    analysis/stats.json plannedContrasts
  • 2.64 ms (go-nethttp) to 3.60 ms (ts-bun-hono)p99 spread at fixed 300 req/s (quote)
    README.md p99 table / RESULTS.md:37