Rust 2026-10-15·Ievgenii Svyryd

Benchmark Round 2: Rust, Go and the Rust Frameworks, Profiled

Go and seven Rust apps (axum, actix-web, Rocket, Loco, Cot and Rustango on axum and actix) under the same rules, with only the app under test running, and the CPU, syscall and wire profiles that show where each one spends its time.

Benchmark Round 2: Rust, Go and the Rust Frameworks, Profiled

This is round 2 of the fair benchmark. Round 2 covers the compiled apps only: Go and seven Rust apps. It adds three Rust frameworks (Rocket, Loco, Cot) and CPU, syscall and wire profiles that show where each app spends its time.

Measured on 14 October 2026 on an Apple M3 Pro (Docker VM with 12 CPUs and 16 GB).

  • 441/s Go garbage collections on the cached index
  • 3 vs 9 Postgres round trips per detail page: Go vs sqlx with the acquire ping
  • +45% Rustango on actix, index page, with one pool slice per worker
  • 6.3–22.8 MiB memory under load, all eight apps
  • 0 errors or non-200 responses in 144 runs

The setup

The same blog as round 1: 10 authors, 30 tags, 1 000 posts, 2 600 post-tag links and 10 000 comments. Six routes: the index (latest 20 posts), a post detail page with all its comments, a tag page, the index and detail page from a Redis page cache, and /compute (sum of primes below 20 000, no database).

Eight apps:

  • Go: stdlib net/http, pgx v5, go-redis
  • axum + sqlx and actix + sqlx: raw sqlx with Go's SQL text, Tera templates
  • Rocket: rocket_db_pools (sqlx) and rocket_dyn_templates (Tera)
  • Rustango on axum and on actix-web: the Rustango ORM and its page cache
  • Loco: SeaORM, Tera
  • Cot: Cot's ORM, askama templates

The rules:

  • The app gets 4 CPUs (pinned to cores 0–3) and 2 GB RAM, with 4 workers. PostgreSQL 16 and Redis 7 run on cores 4–7, the load generator oha on cores 8–11.
  • Only the app under test runs; the other apps are stopped. Between apps, the number of Postgres connections goes back to its baseline.
  • Pools: 16 connections max, 4 min.
  • Before any load, every page of every app is compared with Go's HTML. A mismatch stops the run.
  • No compression. 5 s warm-up, then three 20 s runs per endpoint at 50 connections; the median counts.

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

Same harness as round 1. Round 2 uses PostgreSQL and Redis only (no MySQL).

Versions

  • Go 1.25.14, pgx v5.7.2, go-redis v9.7.0
  • rustc 1.99.0; axum 0.8.9, actix-web 4.16.0, tokio 1.53, sqlx 0.8.6, Tera 1.20.1
  • Rocket 0.5.1 (sqlx 0.7.4, hyper 0.14)
  • Loco 1.2.0 (SeaORM 2.0.4, sqlx 0.9.0)
  • Cot 0.7.0 (sqlx 0.9.0, askama 0.16.1)
  • Rustango with no acquire ping, a page cache that stores raw bytes and one pooled connection per request (finding 5)

Settings that could not be equalized

SettingAppsWhy
Pool pings Postgres on every checkout (sqlx test_before_acquire)Rocket, Loco, CotNo setting to turn it off. It is off in axum + sqlx, actix + sqlx and Rustango.
10 pool connections instead of 16CotCot has no pool settings.
One pool slice per worker (4 of the 16 connections) and one Redis connection per workeractix + sqlx, Rustango · actixA shared pool is slower on actix-web (see finding 6).
sqlx 0.7.4 and hyper 0.14RocketPinned by Rocket 0.5.1.
5, 5 and 7 queries per index, detail and tag page instead of 3, 3 and 4CotIts query API cannot express Go's queries (see finding 9).
askama instead of TeraCotCot's template engine.

Results

Requests per second, higher is better.

Uncached pages

Same HTML, 50 connections, PostgreSQL on every request

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Index
Go 8 741 req/s
axum + sqlx 14 140 req/s
actix + sqlx 14 408 req/s
Rocket 13 316 req/s
Rustango · axum 14 686 req/s
Rustango · actix 17 401 req/sfastest
Loco 10 290 req/s
Cot 2 018 req/s
Detail
Go 22 607 req/s
axum + sqlx 22 842 req/s
actix + sqlx 21 630 req/s
Rocket 24 561 req/s
Rustango · axum 25 788 req/s
Rustango · actix 27 292 req/sfastest
Loco 15 635 req/s
Cot 9 282 req/s
Tag
Go 6 386 req/s
axum + sqlx 6 467 req/s
actix + sqlx 6 628 req/s
Rocket 6 836 req/s
Rustango · axum 7 090 req/sfastest
Rustango · actix 7 056 req/s
Loco 5 604 req/s
Cot 3 065 req/s

Median of three 20 s runs at 50 connections. Only the app under test was running. 0 non-200 responses, 0 errors.

Cached pages

Served from a Redis page cache, no SQL

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Index, cached
Go 91 839 req/s
axum + sqlx 198 669 req/s
actix + sqlx 226 887 req/s
Rocket 138 224 req/s
Rustango · axum 174 501 req/s
Rustango · actix 231 975 req/sfastest
Loco 153 243 req/s
Cot 190 245 req/s
Detail, cached
Go 141 574 req/s
axum + sqlx 214 871 req/s
actix + sqlx 240 161 req/s
Rocket 155 691 req/s
Rustango · axum 192 897 req/s
Rustango · actix 242 609 req/sfastest
Loco 170 158 req/s
Cot 203 119 req/s

Median of three 20 s runs at 50 connections. Only the app under test was running. 0 non-200 responses, 0 errors.

Compute

Primes below 20 000, no database

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 16 580 req/sfastest
axum + sqlx 14 963 req/s
actix + sqlx 14 578 req/s
Rocket 14 696 req/s
Rustango · axum 14 799 req/s
Rustango · actix 14 710 req/s
Loco 14 184 req/s
Cot 15 330 req/s

Median of three 20 s runs at 50 connections. Only the app under test was running. 0 non-200 responses, 0 errors.

Latency, p50 / p99 in ms:

AppIndexDetailTagIndex, cachedDetail, cachedCompute
Go5.50 / 9.961.91 / 5.027.61 / 11.720.35 / 2.730.24 / 2.052.37 / 10.75
axum + sqlx3.49 / 4.712.14 / 3.607.42 / 13.890.24 / 0.530.22 / 0.513.35 / 6.07
actix + sqlx3.32 / 6.382.21 / 4.307.23 / 13.230.20 / 0.500.19 / 0.453.41 / 4.97
Rocket3.66 / 5.831.97 / 3.587.14 / 11.550.34 / 0.700.30 / 0.673.40 / 6.14
Rustango · axum3.28 / 5.651.83 / 3.556.90 / 11.180.27 / 0.590.25 / 0.523.38 / 6.09
Rustango · actix2.80 / 4.511.77 / 3.376.91 / 11.530.20 / 0.480.19 / 0.453.41 / 3.96
Loco4.62 / 10.842.99 / 6.348.67 / 14.520.30 / 0.790.28 / 0.623.48 / 6.90
Cot23.35 / 50.365.28 / 8.0215.57 / 27.060.25 / 0.500.23 / 0.523.27 / 5.85

Memory

Memory under load

Resident memory of the app container on the uncached index

MiB · lower is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 22.8 MiB
axum + sqlx 7.6 MiB
actix + sqlx 7.2 MiB
Rocket 8.7 MiB
Rustango · axum 6.9 MiB
Rustango · actix 6.3 MiBlowest
Loco 12.0 MiB
Cot 22.6 MiB

docker stats, the sample with the highest CPU out of six taken during the first index run.

Go and Cot use the most. Cot's index loads all 1 000 posts with their bodies on every request (finding 9).

Findings

Each finding says what was measured, why it happens and how that was shown. Profiles are single 25 s runs on the same harness, one app at a time, so their req/s differ a little from the fair run above. Where a cause is not proven, it says so.

1. Compute: Go is 8–17% faster

What. Go serves 16 580 req/s on /compute; the Rust apps serve 14 184–15 330.

Why. Both compiled loops have 8 instructions, a 64-bit sdiv and one divide-by-zero check. The difference is the order of the instructions that LLVM picks. Which micro-architectural detail of the CPU makes that order slower is unproven: the Docker VM has no hardware counters.

How proven. The loop alone, on one core, outside HTTP:

Prime loop, one call

Single core, no HTTP

microseconds · lower is better
  • Go
  • Rust
Go 194 microsecondslowest
Rust 224 microseconds
Rust, Go's instruction order 194 microsecondslowest

Rust with Go's exact instruction order runs in Go's time. The rest of each request is cheaper in Rust: 8 µs against Go's 14 µs.

Latency. Go has the lower p50 but the higher p99. Mean latency is 50 / req/s for every app, so the averages follow throughput; the difference is the spread. Go serves requests out of order: p10 0.56 ms, p99 10.75 ms. The Rust apps serve close to first-in, first-out: axum + sqlx p10 1.85 ms, p99 6.07 ms. Why Go's p99 sits near 10 ms is unproven.

2. Cached pages: Go's garbage collector

What. On the cached index Go serves 91 839 req/s; axum + sqlx serves 198 669.

Why. Go runs 441 garbage collections per second on this route, and the stop-the-world pauses add up to about 10% of the time. The rest is CPU per request: 27.2 µs in Go against 14.6 µs in axum + sqlx.

How proven. Raising GOGC removes most of the gap:

Go, cached index, GC tuned

Before: default GOGC=100. Profiling runs.

beforeafter
GOGC=400, req/s 97 289 req/s 144 083 req/s +48%
GC nearly off, req/s 97 289 req/s 159 734 req/s +64%
GC nearly off, p99 2.48 ms 0.90 ms −64%

Where the remaining CPU goes, per request on the cached index:

Goaxum + sqlx
App CPU27.2 µs14.6 µs
Kernel write path10.6 µs6.9 µs
Syscalls10.7 (6.6 read, 2.9 write)4.8 (2.0 recvfrom, 1.4 writev)
Redis CPU6.8 µs2.5 µs
Redis trafficgo-redis: one GET per requestone connection, GETs from many requests batched in one pipeline

Go: CPU per cached request

27.2 µs in total

  • 39%Kernel write path, 10.6 µs
  • 61%Everything else, 16.6 µs

axum + sqlx: CPU per cached request

14.6 µs in total

  • 47%Kernel write path, 6.9 µs
  • 53%Everything else, 7.7 µs

3. Detail page: Postgres round trips

What. Detail page: Go 22 607, axum + sqlx 22 842, Rocket 24 561, Rustango · axum 25 788 req/s. All of them send the same three queries.

Why. The number of round trips to Postgres differs. pgx sends one per query. sqlx adds one when a connection is taken from the pool (the acquire ping) and one when it goes back. axum + sqlx takes a connection per query, so it pays per query; Rocket and Rustango take one connection per page, so they pay once. Each extra round trip costs about 10 µs of Postgres CPU.

How proven. Postgres wire capture during load, counting ReadyForQuery messages per page:

Postgres round trips per detail page

Wire capture, same three queries; ping = sqlx acquire ping

round trips · lower is better
  • Go
  • Rustango
  • Rust
Go · pgx 3 round tripslowest
Rustango 4 round trips
Rocket 5 round trips
axum + sqlx, ping off 6 round trips
axum + sqlx, ping on 9 round trips

The index page shows the same counts.

axum + sqlx: acquire ping turned off

Profiling runs

beforeafter
Detail, req/s 18 222 req/s 23 472 req/s +29%
Detail, Postgres CPU 166.6 µs/req 134.3 µs/req −19%
Index, req/s 12 140 req/s 14 419 req/s +19%
Index, Postgres CPU 237.9 µs/req 208.5 µs/req −12%

Three fewer round trips save 29–32 µs of Postgres CPU per page: about 10 µs each.

Go has the fewest round trips but is not the fastest here: its garbage collector stops it about 9% of the time on this route.

4. Index page: GC and template rendering

What. Index page: Go 8 741 req/s, axum + sqlx 14 140.

Why. Two measured causes. Go's garbage collector again (pauses add up to about 8% of the time on this route), and template rendering: Go's html/template uses reflection and costs 167 µs per request; serde + Tera in Rust cost 72 µs.

How proven.

Go, uncached index, GC tuned

Before: default GOGC=100. Profiling runs.

beforeafter
GOGC=400, req/s 9 359 req/s 12 848 req/s +37%
GC nearly off, req/s 9 359 req/s 13 967 req/s +49%

Template rendering, index page

CPU per request, from the CPU profile

microseconds · lower is better
  • Go
  • Rust
Go · html/template 167 microseconds
Rust · serde + Tera 72 microsecondslowest

Where axum + sqlx spends CPU on the index, from its profile (the parts overlap: allocations happen inside sqlx and Tera):

  • sqlx: about 112 µs per request, of which about 25 µs is the ping when a connection goes back to the pool
  • glibc malloc: about 56 µs
  • kernel sendto: 38 µs

5. Rustango: what the fixes changed

What. Profiles of Rustango before these fixes showed four costs. Three are fixed:

Cost beforeCPU shareFix
A pool checkout for every query19–35%One pooled connection per request: detail +26%
Ping to Postgres on every checkout8–16%Ping off by default
CachePageLayer stores pages as JSON + base64~12–20% per cache hitPages stored as raw bytes
Querysets rebuilt and compiled on every request~2%Not fixed yet

Rustango · axum, before → after the fixes

Profiling runs, one app at a time

beforeafter
Index, req/s 11 215 req/s 15 849 req/s +41%
Detail, req/s 17 267 req/s 28 097 req/s +63%
Index, app CPU 286.6 µs/req 220.8 µs/req −23%
Detail, app CPU 164.7 µs/req 110.4 µs/req −33%

In the fair run, Rustango on actix-web and on axum are the two fastest apps on all three uncached pages.

Open. On the cached index Rustango · axum serves 174 501 req/s against axum + sqlx's 198 669. This gap has not been profiled yet.

6. actix-web: a shared pool is slow

What. In round 1 the actix-web apps trailed axum on uncached pages and used under 300% of their 400% CPU budget.

Why. With one shared pool, every query wakes a thread of another runtime. Giving each actix worker its own pool slice (4 of the 16 connections) and its own Redis connection fixes it. The mechanism, that a socket is driven by the runtime that opened it, is an inference; the effect is measured.

How proven. Same apps, shared pool against per-worker slices:

Shared pool → one pool slice per worker

Profiling runs

beforeafter
actix + sqlx
Index 11 094 req/s 15 108 req/s +36%
Detail 18 080 req/s 23 330 req/s +29%
Index, cached 192 604 req/s 238 884 req/s +24%
Rustango · actix
Index 12 052 req/s 17 454 req/s +45%
Detail 23 032 req/s 28 693 req/s +25%
Index, cached 190 889 req/s 237 962 req/s +25%
Context switches per requestShared poolPool per worker
actix + sqlx, index4.041.45
actix + sqlx, detail3.571.64
Rustango · actix, index2.700.81
Rustango · actix, detail2.341.03

About 2.5× fewer context switches per request. More workers do not help: 8 workers with a shared pool reach 11 904 req/s on the actix + sqlx index (4 workers, shared: 11 094).

7. Rocket: strong on database pages, slowest cached

What. Rocket: detail 24 561 req/s, ahead of Go and the raw sqlx apps; cached index 138 224, the slowest Rust result.

Why. On database pages Rocket takes one connection per page and makes 5 round trips (finding 3). On cached pages it spends 24.1 µs of CPU per request against axum + sqlx's 14.6 µs, with 8.1 syscalls per request against 4.8. The profile shows an extra writev (2.24 per request against 1.36) and contention in malloc; futex calls are 2.88 per request against 0.91.

How proven. CPU accounting, syscall counts and CPU profiles on the cached index.

8. Loco

What. Loco: index 10 290, detail 15 635, cached index 153 243 req/s.

Why. SeaORM sends the same number of queries as Go. On cached pages Loco spends 21.0 µs per request: 2.4 µs more in malloc and 1.2 µs more in middleware than axum + sqlx. On database pages the acquire ping cannot be turned off (finding 3 shows its cost in sqlx).

How proven. CPU accounting and CPU profiles on the cached index. Loco's database pages were not profiled, so how much of that gap the ping explains is unproven.

9. Cot: the query API sets the limit

What. Cot: index 2 018, detail 9 282, tag 3 065 req/s, but cached index 190 245.

Why. Cot's query API has only filter, limit and offset: no ORDER BY, JOIN, IN or COUNT. To show the latest 20 posts, the index loads all 1 000 posts with their bodies. Pages need 5, 5 and 7 queries instead of 3, 3 and 4. Memory under load is 22.6 MiB. Cached pages do not touch the ORM and are fast: 16.4 µs of CPU per request.

How proven. Query counts from the code, memory from the fair run, CPU accounting on the cached index.

10. Memory

See the memory chart above. Six of the Rust apps use 6.3–12.0 MiB under load. Go uses 22.8 MiB. Cot uses 22.6 MiB; its index loads every post on every request (finding 9).

11. Shared costs

Two costs show up in every Rust app and belong to no framework:

  • Tera rendering: about 18–20% of CPU on the uncached pages
  • glibc malloc: about 12%

A faster allocator such as mimalloc or jemalloc could cut the second one. This is untested.

Limits

  • One host: an Apple M3 Pro. Other CPUs can order the results differently, especially compute (finding 1).
  • A read-heavy workload: no writes, no transactions.
  • The apps run in the Docker VM on macOS, not on bare-metal Linux. The VM has no hardware performance counters.

Conclusion: why Rustango on actix-web got so much faster

Rustango · actix, index page

Three runs: round 1, a profiling run, and this round's fair run

req/s · higher is better
Round 1 (0.60.6) 6 341 req/s
Fixes, shared pool 12 052 req/s
Fixes, pool per worker 17 401 req/sfastest

Two changes, in two places.

1. Rustango fixes. These help on axum and on actix-web:

  • Each column is decoded once, by its type. Before, decoding took 46% of the CPU.
  • No ping before each pool checkout: one less Postgres round trip per query.
  • One pooled connection per request: a page's queries share one connection, instead of one checkout and one return ping each.

2. The actix-web setup. axum runs one shared, multi-threaded Tokio runtime. actix-web runs one single-threaded runtime per worker. With one pool shared by all workers, a connection's socket stays driven by the runtime that opened it, so when another worker uses that connection, every read and write wakes the other thread. The app then makes 2.5× more context switches and uses about 280% of its 4 CPUs. One pool slice (4 of the 16 connections) and one Redis connection per worker remove these cross-thread hops:

  • Rustango · actix: index 12 052 → 17 454, detail 23 032 → 28 693, cached index 190 889 → 237 962 req/s
  • actix + sqlx: index 11 094 → 15 108 req/s
  • 8 workers with a shared pool did not help: 11 904 req/s on the actix + sqlx index

The speed-up and the drop in context switches are measured. The socket ownership is the best explanation, but it is not proven directly. axum does not have this problem, because all its tasks run on one shared runtime.

In practice: on actix-web, create the database pool inside each worker, not once for all workers.

Get in touch →