Performance
Measured on the work a real API does.
A CRM-style API with JWT auth, validation, pagination and inserts, on 8 CPUs against PostgreSQL. Navree serves 15,458 authenticated, paginated lists and 20,295 validated inserts a second, peaks at 37.5 MB and answers its first request 23 ms after it starts.
Every number on this page is read from one result file when the site is built. One command repeats the run.
- Measured October 26, 2026
- Navree 0.1.0
- Rust 1.99.1
- PostgreSQL 17
- Authenticated list
- 15,458req/s
- Validated insert
- 20,295req/s
- Plain JSON
- 214,257req/s
- Single query
- 46,547req/s
- Peak memory
- 37.5MB
- Time to ready
- 23ms
Median of 3 × 15 s runs · 256 connections · 8 CPUs · 0 errors
Each figure is the median of 3 runs of 15 seconds at 256 open connections, after a 10-second warm-up. None of the 15 runs returned an error.
JSON
GET /json
Single query
GET /db
20 queries
GET /queries
Auth + paginated list
GET /api/deals
Auth + validated insert
POST /api/deals
Latency
The time each request took, measured by the load generator with every connection busy. The median sits between 1.1 ms for plain JSON and 40 ms for twenty queries in a row.
p99 stays within 2.3× of the median in every test: no long tail.
- p50 (median)
- p99
Every percentile, as a table
| Test | p50 | p75 | p90 | p99 | p99.9 | max |
|---|---|---|---|---|---|---|
| JSON | 1.1 | 1.5 | 1.8 | 2.6 | 3.2 | 5.8 |
| Single query | 5.3 | 5.6 | 6.0 | 8.3 | 10 | 13 |
| 20 queries | 40 | 41 | 43 | 60 | 74 | 77 |
| Auth + paginated list | 16 | 17 | 17 | 22 | 25 | 29 |
| Auth + validated insert | 12 | 13 | 14 | 19 | 26 | 30 |
Milliseconds, from the median run of each test.
Where the CPU goes
On plain JSON the app used 7.7 of its 8 CPUs while PostgreSQL did nothing, so that test is Navree's request pipeline alone.
The app's CPUs are the busiest of the three in every test. Navree's own per-request work sets the pace, which is what the next round of work goes after.
| Test | Navree8 CPUs | PostgreSQL10 CPUs | Load generator6 CPUs |
|---|---|---|---|
| JSON | Navree7.7 of 8 | PostgreSQL0.0 of 10 | Load generator3.1 of 6 |
| Single query | Navree5.9 of 8 | PostgreSQL3.3 of 10 | Load generator0.8 of 6 |
| 20 queries | Navree6.6 of 8 | PostgreSQL7.6 of 10 | Load generator0.2 of 6 |
| Auth + paginated list | Navree7.0 of 8 | PostgreSQL6.0 of 10 | Load generator0.5 of 6 |
| Auth + validated insert | Navree6.1 of 8 | PostgreSQL5.6 of 10 | Load generator0.6 of 6 |
Memory and startup
One process, measured from outside the container: what the app holds at rest, at its busiest, and how soon it answers.
Peak memory under load
37.5MB
The highest reading in any test, sampled with docker stats while the load ran. At rest the process holds 4.2 MB.
- JSON16.3 MB
- Single query35.4 MB
- 20 queries37.5 MB
- Auth + paginated list36.6 MB
- Auth + validated insert36.3 MB
- Idle4.2 MB
Time to ready
23ms
From the container starting to the first 200 from GET /json. In that time the app builds its module graph, wires every provider and starts listening.
- Idle memory
- 4.2 MB
- Processes
- 1
The app is an ordinary Navree application: modules and controllers, the JWT strategy and CurrentUser, validated extractors, SeaORM entities, the unit of work and Navree's pagination. It reads a seeded PostgreSQL database of 10,000 world rows, 100 users and 1,000 deals.
GET
/jsonPlain JSON
Serializes a small struct per request. No database: this is routing and Navree's request pipeline on their own.
GET
/dbSingle query
Reads one random row of
worldby primary key, straight from the pool. A single read needs no transaction.GET
/queries?queries=2020 queries
Twenty separate random-row reads in a plain loop, one after another, with no batching.
GET
/api/deals?page=P&per_page=20Authenticated, paginated list
What a typical authenticated list endpoint does. The load generator cycles through pages 1 to 50.
- 01Verify the bearer token: HS256 signature,
expandiss. - 02Load the user by the token's
subwith theCurrentUserextractor. - 03Validate
page≥ 1 andper_page1 to 100 withValid<Query<T>>. - 04In the request's transaction: count the deals, load the page ordered by
created_at DESC, id DESC, then the page's owners in one query. - 05Respond
200with{ data, meta }, Navree's page shape.
- 01Verify the bearer token: HS256 signature,
POST
/api/dealsAuthenticated, validated insert
Every request creates a new deal, with a fresh title, amount and stage.
- 01Verify the bearer token and load the user, as above.
- 02Validate the body with
Valid<Json<T>>: a title of 1 to 200 characters, an amount from 0 to 10¹² cents, one of six stages. - 03
INSERT … RETURNINGin the request's unit of work, committed when the response succeeds. - 04Respond
201with the deal and aLocationheader.
//! `/api/deals`.
use navree::prelude::*;
use super::dto::{CreateDeal, DealsQuery};
use super::resource::DealResource;
use super::service::DealService;
/// `/api/deals` (no API version): any signed-in user. The JWT strategy
/// authenticates the bearer token in the pipeline; [`CurrentUser`] then
/// loads the user row (401 when it no longer exists).
#[injectable]
pub struct DealsController {
deals: Arc<DealService>,
}
#[controller("/deals")]
impl DealsController {
/// A page of deals, newest first.
#[get("/")]
#[authenticated]
async fn index(
&self,
uow: Uow,
CurrentUser(_user): CurrentUser,
Valid(Query(query)): Valid<Query<DealsQuery>>,
) -> Result<Page<DealResource>> {
self.deals.list(&uow, &query).await
}
/// Creates a deal owned by the signed-in user.
#[post("/")]
#[authenticated]
async fn store(
&self,
uow: Uow,
CurrentUser(user): CurrentUser,
Valid(Json(input)): Valid<Json<CreateDeal>>,
) -> Result<Created<DealResource>> {
let deal = self.deals.create(&uow, &user, input).await?;
Ok(Created(format!("/api/deals/{}", deal.id), deal))
}
}The code being measured, from bench/frameworks/navree. Its README lists every version and setting.
The full default pipeline, on every request
Nothing is switched off for speed. Every request passes through Navree's default layers, outermost first, then the router, the route's access check and the handler.
The rate limit stays in the pipeline with a very high ceiling, because the load generator is one client IP.
- 01Request contextRequest id, client IP, tracing span
- 02Panic catcherA panic becomes a 500
- 03Timeout30 seconds
- 04Security headersHSTS, CSP, frame and referrer policy
- 05CompressionWhen the client asks for it
- 06Error renderingOne JSON error shape
- 07Maintenance modeChecked at most once a second
- 08Rate limitPer client IP
- 09Body limit2 MiB
- 10AuthenticationThe JWT strategy
- 11Unit of workCommit below 400, roll back above
- 12Your handlerAfter the router and the access check
The app runs alone, pinned to 8 CPUs. PostgreSQL and the load generator have CPUs of their own, so nothing competes with the app for time.
Load generator
wrk 4.2.0
6 CPUs · 18–23
- 6 threads, 256 connections
- Monotonic clock
- 10 s request timeout
The app
Navree, one binary
8 CPUs · 0–7
- Tokio, 8 worker threads
- 4 GB memory limit
- Release build, log level warn
Database
PostgreSQL 17
10 CPUs · 8–17
- Data on tmpfs
- synchronous_commit = off
- Fresh copy of the seeded database
Intel Core Ultra 9 275HX · 24 CPUs · Docker 29.9.2 · Docker Desktop on WSL2 · one Docker bridge network
How a run goes
- 01
Build
A release build (
opt-level = 3, thin LTO, one codegen unit) in a slim Debian image. - 02
Fresh data
The database is recreated from the seeded template before the app starts, so every run sees the same rows.
- 03
Start
The clock runs from the container starting to the first
200fromGET /json. Idle memory is read once it answers. - 04
Conformance
170 checks against the API contract: response shapes, status codes, every way a token can be wrong, validation failures and pagination edges. One failure and nothing is measured. Navree passed 170 and failed 0.
- 05
Warm up
10 seconds of load per test, not measured.
- 06
Measure
3 runs of 15 seconds per test with wrk, 6 threads and 256 connections. The median run is reported; memory and CPU are sampled with
docker statsthroughout.
wrk on the monotonic clock
Stock wrk times requests with the wall clock. The VM under Docker Desktop steps its wall clock now and then, which showed up as phantom timeouts. The harness builds wrk 4.2.0 with every clock read switched to CLOCK_MONOTONIC.
Production settings
Release build, log level warn (no per-request log line), one pool of 64 PostgreSQL connections, and Navree's default pipeline on, as above.
Laptop numbers
One Intel Core Ultra 9 275HX laptop with 24 CPUs, Docker Desktop on WSL2, one Docker bridge network. A server will give different absolute figures; the method and the shape of the results carry over.
Steady from run to run
The three runs of each test land close together. Navree was then measured again 78 minutes later, after the machine had been under load the whole time: every test within 3.1% of the first run.
The machine held steady for the whole session: no thermal slowdown.
- The 3 measured runs
- Repeat, 78 minutes later
- JSON: runs at 214,257, 212,906, 219,605 requests a second; the repeat at 220,846, +3.1% against the median.
- Single query: runs at 46,302, 46,547, 47,157 requests a second; the repeat at 46,921, +0.8% against the median.
- 20 queries: runs at 6,126, 6,208, 6,066 requests a second; the repeat at 6,175, +0.8% against the median.
- Auth + paginated list: runs at 15,498, 15,398, 15,458 requests a second; the repeat at 15,449, −0.1% against the median.
- Auth + validated insert: runs at 20,347, 20,295, 20,037 requests a second; the repeat at 20,190, −0.5% against the median.
Navree's build
The versions and settings of the measured app, read from its README.
- Framework
- Navree 0.1.0 from this repository (
crates/navree, a path dependency; featuresauth,log,postgres) - Runtime
- Rust 1.99.1 (
rust:1-bookwormbuild stage,debian:bookworm-slimruntime); release profileopt-level = 3,lto = "thin",codegen-units = 1 - HTTP
- navree-http on Axum 0.8.9, hyper 1.12.0, tower-http 0.7.1
- Workers
- One process.
AppBuilder::runbuilds Tokio's (1.54.1) default multi-thread runtime: one worker per CPU, 8 under the benchmark's cpuset. Theservecommand runsaxum::serveon0.0.0.0:8080 - Database
- navree-db
DatabaseModule: SeaORM 2.0.4 on sqlx 0.9.0 (PostgreSQL), one pool of 64 connections (database.max_connections = 64,migrate_on_boot = false) - JWT
- navree-auth
AuthModule: itsJwtStrategy(jsonwebtoken 11.1.0 with navree-auth's HMAC-only crypto provider on RustCryptohmac/sha2(the measured run used jsonwebtoken'srust_cryptobackend, which signs HS256 with the same crates); HS256, signature,exp,iss = navree, token typeaccess), then theCurrentUserextractor loads theusersrow (navree-auth'suserentity) - Validation
- navree-validation
Valid<Query<T>>andValid<Json<T>>on garde 0.23.0; failures are 400s in Navree's error format
Efficiency
Why it stays lean.
None of this is a benchmark setting. It is how every Navree app runs.
- 01
One binary
Your app, its modules and the framework compile to one native executable. Deploy the file; there is no runtime to install beside it.
Ready in 23 ms · idle at 4.2 MB
- 02
Tokio underneath
One multi-threaded runtime with a worker per CPU. Every connection and every request is a lightweight task, so open connections cost no threads.
8 workers · 256 connections
- 03
Axum and Hyper
Hyper speaks HTTP, Axum routes, and Navree's request pipeline is a stack of Tower layers around your handlers.
214,257 JSON responses a second
- 04
Pooled connections
One pool of PostgreSQL connections, kept open and shared by every worker. SeaORM builds the queries and sqlx runs them.
46,547 single queries a second
- 05
No garbage collector
Rust frees memory the moment a value goes out of scope. There are no collection pauses to tune, and memory tracks the work in flight.
37.5 MB at peak · p99 within 2.3× of p50
- 06
Async all the way down
The HTTP stack, the pool and every query are async, so a worker never sits blocked while PostgreSQL answers. It serves another request.
6,126 requests a second at 20 queries each
Navree's own per-request work is where the time goes: on a single query the app used 5.9 of its 8 CPUs while PostgreSQL used 3.3 of 10. Reading the code turned up eight costs, and removing each one takes work off every request it touches.
- 01
A ping before every query
The pool keeps sqlx's
test_before_acquireon, so each checkout pings PostgreSQL first. A single read costs two round trips instead of one.navree-dbEvery test that reads the database
- 02
An extra round trip per transaction
Every unit-of-work transaction sends a separate
SELECT set_config(…)for the actor and the request id before its first query.navree-dbBoth deals endpoints
- 03
A task for every request
The unit-of-work layer spawns a task per request so it completes even if the client disconnects, including on
/json, where there is nothing to commit.navree-dbEvery test
- 04
Process-wide locks on the hot path
Maintenance mode takes a
Mutexand anRwLock, and the per-IP rate limiter one globalMutex<HashMap>, on every request.navree-httpEvery test
- 05
Metrics work with no recorder
The request-context layer builds metric labels as fresh
Strings even when no metrics recorder is installed, along with a request scope and a UUID request id.navree-httpEvery test
- 06
A wide user lookup
CurrentUserfindsAuthServiceby scanning every provider, then loads every user column, including the password hash and two-factor secrets it doesn't need.navree-authBoth deals endpoints
- 07
JSON bodies parsed twice
A body is parsed into a
serde_json::Valuefirst, then into the DTO.navree-validationThe insert
- 08
Change events nobody hears
Repo::insertserializes each new row for a change event even when nothing listens for change events.navree-dbThe insert
Reproduce
Run it yourself.
You need Docker and Node.js. The script starts PostgreSQL, builds wrk and the app, runs the conformance test, measures every test and writes bench/results/navree.json.
git clone https://github.com/NavreeHQ/navree
cd navree
# Build, check conformance, then 5 s per test: a quick check
node bench/run.mjs --smoke --only navree
# The full run: 10 s warm-up, then 3 × 15 s per test
node bench/run.mjs --only navreeFit it to your machine
The defaults expect 24 CPUs. Every setting is a flag:
- --app-cpus 0-7
- The app's CPUs
- --pg-cpus 8-17
- PostgreSQL's CPUs
- --load-cpus 18-23
- The load generator's CPUs
- --runs 3 --duration 15
- Measured runs per test, and seconds each
- --warmup 10
- Seconds of warm-up per test
- --connections 256 --threads 6
- wrk's connections and threads
- --skip-build
- Reuse the images from the last run
The raw results
The files this page is built from, with every run, percentile and sample.
The same pipeline runs in the app you build.
Every layer measured here is on by default in a new Navree project. Start one, put it under load and read your own numbers.