Skip to content

Benchmarks

What is measured

Five apps — a bare dart:io HttpServer, aim_server, shelf + shelf_router, relic, and dart_frog — answer the same four scenarios. Every app is a dart compile exe binary, runs as a single isolate, adds no middleware or logging, and keeps its framework's default configuration. Load comes from oha over loopback on the same machine. Before any app is measured, it must answer all four scenarios identically to the others.

ScenarioRequestResponseWhat it exercises
plaintextGET /Hello, World! (text)Baseline request/response overhead with no parsing.
params_jsonGET /users/42?name=aim{"id":"42","name":"aim"} (JSON)Path parameter extraction, query string parsing, JSON encoding.
post_jsonPOST /json with a JSON bodythe same JSON, echoed backRequest body reading and JSON decoding, on top of encoding.
routes_100GET /r/item100item100 (text)Router lookup cost with 100 registered static routes (/r/item001/r/item100) instead of one.

What is not measured

  • TLS termination.
  • A database or other I/O-bound work.
  • Real network conditions — every run is against 127.0.0.1.
  • More than one isolate.
  • Large request or response bodies.
  • Anything else a real application spends its time on.

These numbers say nothing about database-bound applications. A single machine on a single day is one data point, not a ranking.

Environment

  • Date: 2026-09-24
  • Machine: Apple M5 Pro, 15 cores, 48 GiB, macOS 26.5.1 (arm64)
  • Dart: Dart SDK version: 3.13.3 (stable) (Tue Sep 1 01:07:17 2026 -0700) on "macos_arm64"
  • Aim commit: e845d62
  • Load generator: oha 1.16.0, 64 connections, 10 s per run, median of 5 runs
  • Every app is a dart compile exe binary, one isolate, no middleware; the load generator connects to 127.0.0.1 (dart_frog's generated server listens on all interfaces, the others on loopback only).
  • Startup is the median of three launches after one discarded launch; the first launch of a freshly compiled binary pays a one-time first-execution cost on macOS and is not what a redeploy sees.

Versions

AppPackages
dart_iodart:io only
aimaim_server 0.4.0 (path), aim_core 0.4.0 (path)
shelf_routershelf 1.4.2, shelf_router 1.1.4
relicrelic 1.2.0, relic_core 1.2.0, relic_io 1.2.0
dart_frogdart_frog 1.2.6, shelf 1.4.2

Results

plaintext

GET /

AppRequests/sp50 (ms)p99 (ms)
dart_io21,8672.665.18
aim20,3032.895.39
shelf_router19,6832.975.18
relic19,2813.055.57
dart_frog17,2473.395.96

params_json

GET /users/42?name=aim

AppRequests/sp50 (ms)p99 (ms)
dart_io21,9172.674.87
aim19,7773.025.62
shelf_router18,0873.245.45
relic18,8013.155.42
dart_frog13,9434.226.93

post_json

POST /json

AppRequests/sp50 (ms)p99 (ms)
dart_io21,0492.794.84
aim18,9803.095.70
shelf_router17,1893.465.86
relic17,4193.385.78
dart_frog14,7964.016.77

routes_100

GET /r/item100

AppRequests/sp50 (ms)p99 (ms)
dart_io22,5052.604.92
aim20,0222.915.30
shelf_router15,8893.746.22
relic19,3883.035.58
dart_frog7,2438.4612.63

Footprint

AppStartup to first 200, warm binary (ms)Memory after load (MiB)Binary (MiB)
dart_io18395.8
aim18395.9
shelf_router18226.4
relic17427.6
dart_frog17406.5

Reading the numbers

  • Requests/s and latency are the median of five 10-second runs at 64 connections after a 3-second warm-up; the committed JSON keeps all five runs.
  • In this run every app/scenario stayed within 10% of its median except dart_io on plaintext (12%).
  • Startup is the median of three launches after one discarded launch; the first launch of a freshly compiled binary pays a one-time first-execution cost on macOS and is excluded.
  • Memory is the physical footprint reported by footprint (macOS refuses ps's rss column), reported in whole MiB.
  • dart_frog's generated server listens on all interfaces while the other four bind loopback (defaults kept; the load generator always connects to 127.0.0.1).
  • dart_frog's routes_100 figure reflects that its generated server builds the /r/ directory's router on every request (its default, kept).
  • Apps run in a fixed order (dart_io, aim, shelf_router, relic, dart_frog), so thermal drift over the ~25 minutes would affect later apps more.

What this says about Aim's router

Aim's router is a linear scan of registered routes with the first match winning. In this run, aim's routes_100 median of 20,022 requests/s (100 static routes, last one requested) was about 1% below its plaintext median of 20,303 requests/s. Over bare dart:io, aim's median requests/s was 7% lower on plaintext and 10% lower on params_json and post_json. Run-to-run spread in this data is 2–12%, so differences of a few percent are within noise.

Reproduce

Prerequisites:

  • Dart 3.13 or newer.
  • oha 1.16 or newer: brew install oha.
  • macOS — the runner reads machine info with sysctl/sw_vers and reads a running app's memory with footprint.
bash
cd bench/runner
dart pub get
dart run bin/bench.dart verify        # every app must answer identically
dart run bin/bench.dart run --label <short-description>  # e.g. m5-pro, not the hostname
dart run bin/bench.dart render ../results/<date>-<label>.json

Results files, with all runs kept (not just the median), live under bench/results/. The full suite, including the fairness rules and how to add another app, is on GitHub.

Released under the MIT License.