HTTP framework performance benchmarks comparing ntnt against popular alternatives across real-world workloads.
Language: ntnt v0.4.2 (Rust runtime, Axum/Tokio)
Competitors: Actix Web · Gin · FastAPI · Fastify · Hono/Bun · Django · Express · Rails
Tool: wrk — 4 threads, 100 connections, 15s per run, 3 runs (median)
| Benchmark | ntnt | Actix Web | Gin | FastAPI | Fastify | Hono/Bun | Django | Express | Rails |
|---|---|---|---|---|---|---|---|---|---|
| Plaintext | 302,141 | 476,661 | 406,060 | 173,783 | 81,212 | 118,409 | 17,967 | 18,167 | 10,844 |
| JSON | 295,990 | 476,342 | 387,095 | 151,696 | 78,421 | 105,152 | 17,119 | 17,017 | 10,387 |
| Params | 267,832 | 469,648 | 384,301 | 130,802 | 76,910 | 101,625 | 17,245 | 16,788 | 9,926 |
| JSON Body | 252,284 | 447,592 | 324,507 | 116,140 | 38,456 | 82,927 | 17,111 | 11,828 | 10,941 |
| DB (1 query) | 37,818 | 64,003 | 130,190 | 36,859 | 33,244 | 32,399 | 385 | 11,817 | 7,283 |
| Queries (20) | 2,349 | 3,916 | 9,296 | 5,818 | 3,222 | 2,789 | 348 | 2,419 | 1,578 |
| Template | 4,614 | 7,696 | 18,014 | 10,431 | 5,957 | 5,171 | 434 | 4,112 | 2,676 |
All values are requests/sec (higher is better). Bold = fastest. Median of 3 runs, 15s each.
Interactive charts: results/charts.html
| ID | Route | What it tests |
|---|---|---|
plaintext |
GET /plaintext |
Raw HTTP overhead — parsing, routing, response writing |
json |
GET /json |
JSON serialization on top of plaintext |
params |
GET /users/:id |
Router pattern matching + param extraction |
| ID | Route | What it tests |
|---|---|---|
db |
GET /db |
Single random PostgreSQL row |
queries |
GET /queries?count=20 |
20 sequential individual queries |
template |
GET /template |
10 DB rows rendered as HTML |
json-body |
POST /json |
1KB JSON body parse + echo |
Each framework implements identical endpoints using idiomatic code for that ecosystem — no hand-optimization.
| Framework | Language | Runtime | Port |
|---|---|---|---|
| ntnt | ntnt | Rust (Axum/Tokio) | 3100 |
| FastAPI | Python 3.12 | uvicorn (multi-worker) | 3101 |
| Express | TypeScript | Node.js v22 | 3102 |
| Gin | Go | net/http + goroutines | 3103 |
| Hono | TypeScript | Bun v1.3 | 3104 |
| Actix Web | Rust | Tokio (multi-worker) | 3105 |
| Fastify | JavaScript | Node.js v22 | 3106 |
| Rails | Ruby 3.2 | Puma (4 workers) | 3107 |
| Django | Python 3.12 | gunicorn (4 sync workers) | 3108 |
# Required
sudo apt-get install wrk postgresql-client
# Runtime-specific (install what you want to benchmark)
# ntnt
curl -fsSL https://ntnt-lang.org/install | sh # or build from source
# Python
python3 -m pip install -r fastapi/requirements.txt
# Node.js / TypeScript
cd express && npm install && npx tsc
# Go
go build -o gin/gin-bench ./gin/
# Bun
curl -fsSL https://bun.sh/install | bash
cd hono-bun && bun install
# Rust
cd actix && cargo build --releaseAll DB benchmarks use a shared PostgreSQL instance with the TechEmpower world table (10,000 rows).
# Create the benchmark database
createdb benchmarks
# Seed it
psql -d benchmarks -f setup-db.sqlBy default, implementations connect to 172.19.0.3:5432 (Docker-hosted PG). Override with:
export DATABASE_URL="postgresql://user:pass@host:5432/benchmarks"# Full suite (all frameworks, all benchmarks)
./benchmark.sh
# Custom frameworks/benchmarks
./benchmark.sh --frameworks "ntnt gin actix" --benchmarks "plaintext db queries"
# Faster run (shorter duration)
./benchmark.sh --duration 10 --runs 1
# All options
./benchmark.sh --helpOptions:
| Flag | Default | Description |
|---|---|---|
--frameworks |
all | Space-separated list: ntnt fastapi express gin hono actix fastify rails django |
--benchmarks |
all | Space-separated list: plaintext json params db queries template json-body |
--duration |
30 |
Seconds per wrk run |
--connections |
100 |
Concurrent connections |
--threads |
4 |
wrk thread count |
--runs |
3 |
Runs per benchmark (median is used) |
--warmup |
5 |
Warmup duration in seconds |
--output |
results/ |
Directory for raw output and summaries |
After each run, results are saved to results/:
results/
├── 20260312-095500-summary.md ← Human-readable table
├── 20260312-095500.json ← Machine-readable
└── raw-<framework>-<bench>-run*.txt ← Full wrk output (proof)
- Idiomatic code — each implementation is written the way that framework's own docs recommend. No hand-tuning, no disabling features.
- Same database — shared PostgreSQL instance, same table, same query, 50-connection pool per framework.
- Production mode — debug logging disabled, hot-reload off, production flags set.
- Pinned versions — all runtime and dependency versions are pinned. See each subdirectory.
- Median of 3 — each benchmark runs 3 times; the median result is reported. Eliminates warm-up noise.
- Sequential — frameworks run one at a time, never concurrently, so they don't compete for resources.
- Multi-process vs single-process — FastAPI uses
--workers $(nproc), Rails uses Puma with 4 workers, Django uses gunicorn with 4 sync workers. ntnt, Gin, and Actix are multi-threaded single-process. Express, Fastify, and Hono are single-threaded. - Django sync caveat — Django's DB numbers are limited by synchronous gunicorn workers + psycopg2. An async Django setup (ASGI + asyncpg) would perform significantly better.
- Same machine — benchmarks run on the same host as PostgreSQL. Production would have separate hosts; DB-heavy benchmarks would look different.
- No reverse proxy — no nginx in front. Real deployments add a layer.
- Actix is included as a theoretical ceiling — it shows the interpreter overhead of ntnt vs raw Rust.
Run on: see results/<timestamp>-summary.md for the system info of each run.
Run the suite, open a PR with your results/ output. If you're adding a new framework, add a new subdirectory with the same 7 endpoints and update benchmark.sh.
MIT