Rust 2026-10-13·Ievgenii Svyryd

A Fair Benchmark: Rust, Go, Python, Laravel and WordPress

I re-ran my Rustango benchmark with strict parity: same CPUs, pool, queries and HTML for Go, Rust, Python, Laravel and WordPress. Here is what was wrong the first time, the new numbers, and where my own framework loses time.

A Fair Benchmark: Rust, Go, Python, Laravel and WordPress

A few months ago I published a benchmark that put Rustango, my Django-style framework for Rust, next to Go, a Python framework and Laravel. Go won the uncached pages by a wide margin, and I explained it away. This week I went back to it, and the comparison turned out to be unfair in several ways, most of them against Go and some against Rust.

So I rebuilt the harness from scratch, added plain axum and actix-web next to Rustango, added WordPress, and re-ran everything on one day under one set of rules. This post is the result, including the parts that don't flatter my own framework.

  • +34% plain Rust vs Go on the uncached index
  • 2.2× cached throughput vs Go
  • 0.45 ms p99 on cached pages (Go: 2.0–2.4 ms)
  • 7 MiB RAM under load (Go: 21 MiB)
  • 46% of Rustango's CPU in one function

The workload

Every app serves the same blog: 10 authors, 30 tags, 1 000 posts, 2 600 post-tag links and 10 000 comments, seeded deterministically. Six routes:

  • /: the latest 20 posts with author, tags and comment count
  • /post/{slug}: one post with author, tags and every comment
  • /tag/{slug}: the latest 20 posts carrying a tag
  • /cached and /post/{slug}/cached: the same pages from a Redis page cache
  • /compute: sum of every prime below 20 000 by trial division, no database

Ten runtimes: Go (stdlib net/http + pgx), Rustango on axum and on actix-web, plain axum and actix-web with raw sqlx and Go's exact SQL, a Python framework on gunicorn and on Hypercorn, Laravel on php-fpm and on Octane, and WordPress.

What was wrong the first time

Checking the old run line by line found six problems:

  • Go ran 12 threads on a 4-CPU quota. The module said go 1.23, so Go 1.25 skipped its container-aware GOMAXPROCS. Fixing it alone gave Go +37% on the index and +79% on compute. The 47 ms compute p99 I had blamed on garbage collection was CPU throttling.
  • Rustango ran more queries than Go: 5 instead of 3 on the index, 7 instead of 4 on the tag page, and it loaded every author, every tag and every post body. Matching Go's plan gave +15% on the index and +40% on detail.
  • Different pool sizes: Go had 16 connections, Rustango sqlx's default 10.
  • The load generator ran on the Mac, so every request went through Docker Desktop's port forwarding.
  • "100% success" meant connections, not HTTP 200. The old Hypercorn run had more 500s than good responses: its persistent connections leaked under ASGI until PostgreSQL ran out of slots. Laravel's predis client ran the container out of ports, and Laravel's nginx gzipped its pages, so it sent 700 bytes where everyone else sent 6.4 KB.
  • One 10-second run per endpoint, no warm-up, no repeats.

Two fixes, measured

beforeafter
Go: 12 threads → 4 on a 4-CPU quota
Index 6 814 req/s 9 322 req/s +37%
Compute 9 263 req/s 16 599 req/s +79%
Compute p99 73.8 ms 10.8 ms −85%
Rustango: old queries → Go's query plan
Index 7 385 req/s 8 462 req/s +15%
Detail 12 546 req/s 17 594 req/s +40%

The rules this time

  • Every app gets the same 4 CPUs (pinned to cores 0–3) and 2 GB RAM. Go, Tokio and actix-web run exactly 4 workers.
  • PostgreSQL, MySQL and Redis run on cores 4–7; the load generator, oha, runs on cores 8–11 inside the Docker network.
  • 16 database connections per app. The compiled apps all issue Go's query plan; the Rust ones without an ORM use Go's SQL text verbatim.
  • Before any load, every page from every app is compared with Go's HTML. A mismatch stops the run.
  • No compression anywhere, so every app sends the same bytes.
  • Production settings: OPcache, php artisan optimize, pooled connections, debug off, release builds with LTO.
  • 5 s warm-up, then three 20 s runs per endpoint at 50 connections; the median counts, and only HTTP 200 responses count.

The harness

DOCKER NETWORK · NO COMPRESSION oha load generator 8 9 10 11 CORES 8–11 App under test 4 CPUs · 2 GB · 4 workers 0 1 2 3 CORES 0–3 · PINNED Databases Postgres · MySQL · Redis 4 5 6 7 CORES 4–7 HTTP 50 conns SQL · cache 16 conns ✓ HTML parity vs Go, before any load PER ENDPOINT 5 s run 1 · 20 s run 2 · 20 s run 3 · 20 s WARM-UP → MEDIAN OF 3 · HTTP 200 ONLY DOCKER NETWORK oha load generator CORES 8–11 8 9 10 11 HTTP 50 conns ✓ HTML parity vs Go App under test 4 CPUs · 2 GB · 4 workers CORES 0–3 0 1 2 3 SQL · cache · 16 conns Databases PostgreSQL · MySQL · Redis CORES 4–7 4 5 6 7 PER ENDPOINT 20 s 20 s 20 s 5 S WARM-UP → MEDIAN OF 3

Compiled runtimes

Requests per second, higher is better:

Uncached pages

Same SQL, same HTML, 50 connections

req/s · higher is better
  • Go
  • Rustango
  • Rust
Index
Go 9 360 req/s
Rustango · axum 8 485 req/s
Rustango · actix 6 341 req/s
axum + sqlx 12 505 req/sfastest
actix + sqlx 9 963 req/s
Detail
Go 22 675 req/sfastest
Rustango · axum 17 634 req/s
Rustango · actix 13 883 req/s
axum + sqlx 18 578 req/s
actix + sqlx 14 697 req/s
Tag
Go 6 457 req/sfastest
Rustango · axum 5 577 req/s
Rustango · actix 5 146 req/s
axum + sqlx 6 299 req/s
actix + sqlx 5 903 req/s

Cached pages

Served from a Redis page cache

req/s · higher is better
  • Go
  • Rustango
  • Rust
Index, cached
Go 96 214 req/s
Rustango · axum 164 942 req/s
Rustango · actix 200 167 req/s
axum + sqlx 214 097 req/sfastest
actix + sqlx 197 252 req/s
Detail, cached
Go 143 795 req/s
Rustango · axum 194 939 req/s
Rustango · actix 214 662 req/s
axum + sqlx 233 669 req/sfastest
actix + sqlx 211 293 req/s

Compute

Primes below 20 000, no database

req/s · higher is better
  • Go
  • Rustango
  • Rust
Go 16 588 req/sfastest
Rustango · axum 14 733 req/s
Rustango · actix 15 137 req/s
axum + sqlx 15 085 req/s
actix + sqlx 14 741 req/s
  • Rust vs Go with the same SQL is a draw with character. Plain axum beats Go by 34% on the index, Go beats it by 22% on detail, and the tag page is a tie because PostgreSQL is the bottleneck there (3.65 of its 4 cores busy).
  • Cached pages are where Rust pulls away: 1.5–2.2× Go's throughput, with a p99 around 0.45 ms against Go's 2.0–2.4 ms.
  • Memory: every Rust app runs in 7–8 MiB under load; Go in about 21 MiB.
  • actix-web trails axum on uncached pages with a shared sqlx pool and never uses its full 4 CPUs. I don't know why yet.

The part about my own framework

Rustango on axum is 32% slower than raw sqlx on the same server with the same three queries. That's the honest cost of the ORM today, so I profiled it with perf:

Where Rustango's CPU goes

perf, share of app CPU on uncached pages

  • 46%Postgres values decoded by trying ~10 types per cell (#2655)
  • 6–7%A HashMap with owned keys for every aggregate row (#2661)
  • ~2%Querysets rebuilt and compiled on every request (#2662)
  • restEverything else

Cached hits are a separate profile: CachePageLayer's JSON + base64 is ~21% of each one (#2656).

CostCPU shareTicket
Postgres values decoded by trying ~10 types per cell, each miss building an error46% on the index#2655
CachePageLayer stores pages as JSON + base64~21% per cached hit#2656
A HashMap with owned keys for every aggregate row6–7%#2661
Querysets rebuilt and compiled on every request~2%#2662

Decoding by the known type

Micro-benchmark of the fix for #2655

beforeafter
Posts query decode 87 µs 5 µs −94%

Almost half the CPU goes to one function guessing column types that PostgreSQL already reports. Decoding once by the known type takes the posts query from 87 µs to 5 µs in a micro-benchmark, which should put Rustango close to raw sqlx. Matching Go's queries also showed API gaps: no related columns in values_list (#2657), no = ANY($1) array bind (#2658) and no batch many-to-many prefetch (#2659).

Interpreted runtimes

Same rules, same day. Requests per second:

Uncached pages

Same SQL shape, same HTML, 50 connections

req/s · higher is better
  • Python
  • Python, ASGI
  • PHP · Laravel
  • WordPress
Index
Python · gunicorn 683 req/s
Python · Hypercorn 601 req/s
Laravel · php-fpm 518 req/s
Laravel · Octane 1 098 req/sfastest
WordPress 599 req/s
Detail
Python · gunicorn 2 865 req/sfastest
Python · Hypercorn 1 314 req/s
Laravel · php-fpm 572 req/s
Laravel · Octane 1 712 req/s
WordPress 687 req/s
Tag
Python · gunicorn 1 337 req/sfastest
Python · Hypercorn 786 req/s
Laravel · php-fpm 490 req/s
Laravel · Octane 1 102 req/s
WordPress 647 req/s

Cached pages

Served from a Redis page cache

req/s · higher is better
  • Python
  • Python, ASGI
  • PHP · Laravel
  • WordPress
Index, cached
Python · gunicorn 11 052 req/s
Python · Hypercorn 2 744 req/s
Laravel · php-fpm 3 326 req/s
Laravel · Octane 5 884 req/s
WordPress 21 285 req/sfastest
Detail, cached
Python · gunicorn 11 473 req/s
Python · Hypercorn 2 644 req/s
Laravel · php-fpm 3 013 req/s
Laravel · Octane 5 836 req/s
WordPress 21 591 req/sfastest

Compute

Primes below 20 000, no database

req/s · higher is better
  • Python
  • Python, ASGI
  • PHP · Laravel
  • WordPress
Python · gunicorn 410 req/s
Python · Hypercorn 353 req/s
Laravel · php-fpm 1 611 req/sfastest
Laravel · Octane 1 521 req/s
WordPress 732 req/s
  • Uncached pages: the best interpreted result is 6–11× behind Go and plain Rust on index and detail, and about 5× on the tag page.
  • WordPress wins the cached race among interpreted runtimes. Its advanced-cache.php drop-in answers from Redis before WordPress even loads, at 21 000 requests per second. Uncached, it runs 11–13 queries per page on MySQL (it doesn't support PostgreSQL).
  • Octane is 2–3× php-fpm on uncached pages. Keeping Laravel resident in memory still matters.
  • Hypercorn vs gunicorn: the views are synchronous, so ASGI only adds a threadpool hop — Hypercorn is 2× slower on detail and 4× slower on cached pages than gunicorn running the same app.
  • PHP out-computes CPython about 4× on the integer loop.
  • Memory under load: 89 MiB for Laravel on php-fpm up to 241 MiB for Octane, 224 MiB for the Python app on gunicorn and 296 MiB on Hypercorn. That is 10–40× a Rust binary.

Memory under load

Resident memory of the app container, one scale for all

MiB · lower is better
  • Rust
  • Go
  • PHP · Laravel
  • WordPress
  • Python
  • Python, ASGI
Compiled
Rust apps (all four) 6.9–7.5 MiBlowest
Go 21.2 MiB
Interpreted
Laravel · php-fpm 89 MiBlowest
WordPress 142 MiB
Python · gunicorn 224 MiB
Laravel · Octane 241 MiB
Python · Hypercorn 296 MiB

What I take from it

A fair benchmark first finds the bugs in your benchmark, then the bugs in your code.

Rust and Go are in the same league; neither wins everywhere, and the database decides more than the language on uncached pages. Rust's clearest wins are cached throughput, tail latency and memory. Rustango's job now is to close the 32% gap to raw sqlx, and the profile says most of it is one fix away.

The full tables, latencies, footprints and method are in the Rustango docs.

Зв'язатися →