navree

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
bench/results/navree.json170/170 checks
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.

Requests per second, median of three runs, on one linear scale. Higher is better.

JSON

GET /json

214,257req/s
p501.1 ms
p992.6 ms
Peak memory16.3 MB

Single query

GET /db

46,547req/s
p505.3 ms
p998.3 ms
Peak memory35.4 MB

20 queries

GET /queries

6,126req/s
p5040 ms
p9960 ms
Peak memory37.5 MB

Auth + paginated list

GET /api/deals

15,458req/s
p5016 ms
p9922 ms
Peak memory36.6 MB

Auth + validated insert

POST /api/deals

20,295req/s
p5012 ms
p9919 ms
Peak memory36.3 MB

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.

Latency at 256 open connections, log scale. Lower is better.
  • p50 (median)
  • p99
JSON1.12.6 ms
Single query5.38.3 ms
20 queries4060 ms
Auth + paginated list1622 ms
Auth + validated insert1219 ms
Every percentile, as a table
Testp50p75p90p99p99.9max
JSON1.11.51.82.63.25.8
Single query5.35.66.08.31013
20 queries404143607477
Auth + paginated list161717222529
Auth + validated insert121314192630

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.

Average CPU while each test ran, as a share of the CPUs each container was given.
JSONNavree7.7 of 8PostgreSQL0.0 of 10Load generator3.1 of 6
Single queryNavree5.9 of 8PostgreSQL3.3 of 10Load generator0.8 of 6
20 queriesNavree6.6 of 8PostgreSQL7.6 of 10Load generator0.2 of 6
Auth + paginated listNavree7.0 of 8PostgreSQL6.0 of 10Load generator0.5 of 6
Auth + validated insertNavree6.1 of 8PostgreSQL5.6 of 10Load 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.

0.9% of the limit4 GB container limit
  • 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.

  1. GET/json

    Plain JSON

    Serializes a small struct per request. No database: this is routing and Navree's request pipeline on their own.

  2. GET/db

    Single query

    Reads one random row of world by primary key, straight from the pool. A single read needs no transaction.

  3. GET/queries?queries=20

    20 queries

    Twenty separate random-row reads in a plain loop, one after another, with no batching.

  4. GET/api/deals?page=P&per_page=20

    Authenticated, paginated list

    What a typical authenticated list endpoint does. The load generator cycles through pages 1 to 50.

    1. 01Verify the bearer token: HS256 signature, exp and iss.
    2. 02Load the user by the token's sub with the CurrentUser extractor.
    3. 03Validate page ≥ 1 and per_page 1 to 100 with Valid<Query<T>>.
    4. 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.
    5. 05Respond 200 with { data, meta }, Navree's page shape.
  5. POST/api/deals

    Authenticated, validated insert

    Every request creates a new deal, with a fresh title, amount and stage.

    1. 01Verify the bearer token and load the user, as above.
    2. 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.
    3. 03INSERT … RETURNING in the request's unit of work, committed when the response succeeds.
    4. 04Respond 201 with the deal and a Location header.
bench/frameworks/navree/src/deals/controller.rs

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.

  1. 01Request contextRequest id, client IP, tracing span
  2. 02Panic catcherA panic becomes a 500
  3. 03Timeout30 seconds
  4. 04Security headersHSTS, CSP, frame and referrer policy
  5. 05CompressionWhen the client asks for it
  6. 06Error renderingOne JSON error shape
  7. 07Maintenance modeChecked at most once a second
  8. 08Rate limitPer client IP
  9. 09Body limit2 MiB
  10. 10AuthenticationThe JWT strategy
  11. 11Unit of workCommit below 400, roll back above
  12. 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.

How a run goes

  1. 01

    Build

    A release build (opt-level = 3, thin LTO, one codegen unit) in a slim Debian image.

  2. 02

    Fresh data

    The database is recreated from the seeded template before the app starts, so every run sees the same rows.

  3. 03

    Start

    The clock runs from the container starting to the first 200 from GET /json. Idle memory is read once it answers.

  4. 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.

  5. 05

    Warm up

    10 seconds of load per test, not measured.

  6. 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 stats throughout.

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.

Each run against the reported median, in percent.
  • 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; features auth, log, postgres)
Runtime
Rust 1.99.1 (rust:1-bookworm build stage, debian:bookworm-slim runtime); release profile opt-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::run builds Tokio's (1.54.1) default multi-thread runtime: one worker per CPU, 8 under the benchmark's cpuset. The serve command runs axum::serve on 0.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: its JwtStrategy (jsonwebtoken 11.1.0 with navree-auth's HMAC-only crypto provider on RustCrypto hmac/sha2 (the measured run used jsonwebtoken's rust_crypto backend, which signs HS256 with the same crates); HS256, signature, exp, iss = navree, token type access), then the CurrentUser extractor loads the users row (navree-auth's user entity)
Validation
navree-validation Valid<Query<T>> and Valid<Json<T>> on garde 0.23.0; failures are 400s in Navree's error format

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.

  1. 01

    A ping before every query

    The pool keeps sqlx's test_before_acquire on, so each checkout pings PostgreSQL first. A single read costs two round trips instead of one.

    navree-dbEvery test that reads the database

  2. 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

  3. 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

  4. 04

    Process-wide locks on the hot path

    Maintenance mode takes a Mutex and an RwLock, and the per-IP rate limiter one global Mutex<HashMap>, on every request.

    navree-httpEvery test

  5. 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

  6. 06

    A wide user lookup

    CurrentUser finds AuthService by scanning every provider, then loads every user column, including the password hash and two-factor secrets it doesn't need.

    navree-authBoth deals endpoints

  7. 07

    JSON bodies parsed twice

    A body is parsed into a serde_json::Value first, then into the DTO.

    navree-validationThe insert

  8. 08

    Change events nobody hears

    Repo::insert serializes each new row for a change event even when nothing listens for change events.

    navree-dbThe insert

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.

Terminal

Fit 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 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.