Benchmarking methodology

Measure Bun's startup, throughput, and memory reproducibly without being misled by microbenchmarks

Last updated on

Why this page exists

Nearly every "Bun is N× faster than Node" number comes from a hello-world microbenchmark. Real gains depend on your workload shape: startup-sensitive CLIs, I/O-bound APIs, and CPU-bound transforms produce completely different verdicts. This page is a reproducible measurement process whose output can go into a decision record.

Principles

  1. Measure your own workload: real routes, real dependencies, real data volumes. Hello world only measures framework overhead.
  2. Change one variable at a time: runtime, version, code, data — move exactly one, or you can't attribute the result.
  3. Pin the environment: same machine, same CPU/memory limits, same environment variables; if you deploy in containers, measure with production limits.
  4. Distributions, not averages: report at least p50/p95/p99; mean latency hides the tail.
  5. Warm up before sampling: JIT and connection pools make the first few hundred requests unrepresentative.

Three different measurement targets

TargetToolWatch out for
Startup time (CLIs, cold starts)hyperfine 'bun bench.ts' 'node bench.ts'Run the same file on both runtimes so the runtime is the only variable; hyperfine reports mean±sd and min/max — export --export-json and post-process for percentiles
HTTP throughput and latencyoha / wrkThe client must not bottleneck first; ramp connections to find the knee
Memory and long-run stability/usr/bin/time -v, platform monitoringRun long enough to observe GC and leaks, not startup peaks
# Example: one discarded warmup run, then a concurrency ladder, 30s per step
oha -z 10s -c 50 http://localhost:3000/api/items > /dev/null  # warmup run
oha -z 30s -c 50 http://localhost:3000/api/items
oha -z 30s -c 200 http://localhost:3000/api/items

Bun-specific attribution notes

  • Bun uses JavaScriptCore; Node/Deno use V8. Some code shapes (regexes, specific builtins, numeric work) are fast paths on one engine and not the other. The difference is an engine trait, not necessarily "Bun fast/slow."
  • Install and resolution speed (bun install, cold-start imports) and request-time throughput are separate things — don't conflate them.
  • --watch, --hot, and development modes all have measurable overhead; take every number in production mode.
  • Validate behavior under container CPU limits: runtime thread pools scale to visible cores, so different limits produce different conclusions.

Performance regression in CI

A performance gate isn't about peak scores — it prevents regressions:

  1. Pick one stable, representative scenario (one endpoint or one script);
  2. Pin runner specs and iteration counts; archive results with build artifacts;
  3. Set thresholds above the noise floor (e.g. fail only past 20% over baseline); when results are noisy, add samples before loosening tolerance;
  4. When a regression fires, attribute it one variable at a time: version, code, dependencies, environment.

Report format (agent-friendly too)

Workload: <endpoint/script + input shape>
Environment: <runtime + version, OS/arch, CPU/mem limits>
Command: <exact command>
Runs: <count, warmup>
Result: <p50/p95/p99, throughput, memory>
Conclusion: <what decision this number supports>

A performance number without Workload and Environment enters no decision.

Official references: oha, hyperfine, Bun.serve tuning options.