Skip to content

Benchmarks

mbx is measured against plain Cargo and kache on jdx/hk, a mid-size Rust CLI with C dependencies, pinned to one commit and built with cargo build --locked. The scenarios cover work a developer or CI runner may repeat. The numbers come from a GitHub Actions run, never a laptop, and the page will not name a fastest tool when the gap is inside run-to-run noise.

warm CI rebuilds a commit it has already built

The store is warm from an earlier build of the same commit and target/ is empty. Cargo is left out: with nothing to reuse it would repeat that earlier build.

wall clock · lower is better
ToolRelative wall clockMedian and range
mbxfastest7.8s 7.5–8.3s
kache8.7s 8.5–9.0s

mbx fastest, 0.9s ahead of kache. That lead is wider than either tool's own range across 3 runs.

mbx restored 1,560 output files on 722 cache hits.

commit CI builds the next push

The caches were warmed at the parent commit and build the child. Cargo has no cache to restore, so its row is the uncached build.

wall clock · lower is better
ToolRelative wall clockMedian and range
mbx1.39× faster than Cargo16.7s 16.2–18.6s
kache1.27× faster than Cargo18.3s 17.4–18.9s
Cargouncached23.2s 23.0–24.4s

Too close to call: mbx and kache finished 1.6s apart, inside the 2.3s one of them moved across its own runs.

mbx restored 1,558 output files on 721 cache hits.

edit One line changed, rebuilt in place

The local edit loop with incremental compilation on. Cargo's own incremental rebuild is the thing to beat here, not a control.

wall clock · lower is better
ToolRelative wall clockMedian and range
Cargoincremental rebuild3.7s 3.6–3.8s
mbxlevel with Cargo4.4s 4.4–5.6s
kache1.1s behind Cargo4.8s 4.8–5.1s

Too close to call: Cargo and mbx finished 0.7s apart, inside the 1.2s one of them moved across its own runs.

mbx restored 2 output files on 1 cache hits.

First edit after a build, before the loop settles: Cargo 3.8s, mbx 15.6s, kache 19.1s.

contention Six CI jobs on one runner

Overlapping check, Clippy, and test-compilation jobs. Run one after another, then all at once with and without mbx's machine-wide compiler limit.

wall clock · lower is better
ToolRelative wall clockMedian and range
sequential31 compilers at peak, 63.0 GB free at the low1m 13s 70.0–79.9s
parallelscheduler off187 compilers at peak, 54.1 GB free at the low1m 3s 60.3–64.4s
parallelmbx schedulerfastest32 of 32 permits used, 61.6 GB free at the low41.2s 38.8–42.4s

With the scheduler on, the parallel batch finished 22.1s sooner than unscheduled and 31.9s sooner than running the jobs in turn, peaking at 32 compilers instead of 187.

Cache hits over the batch: sequential 8; parallel, scheduler off 12; parallel, mbx scheduler 3,390. The scheduler holds identical compilations until the first finishes, so the other jobs hit the store instead of repeating the work.

Measured on Linux-7.1.4-x86_64-with-glibc2.39 (nsc-runner-4eu9mf1icdk5o) with Rust 1.97.1, building hk at fc29ead under mbx 1.11.0, cargo 1.97.1 (c980f4866 2026-06-30), rustc 1.97.1 (8bab26f4f 2026-07-14), kache 0.19.0. The run that produced them has the per-build logs.

Reading the results

Every timed scenario runs three times per tool from a fresh clone and an empty store. The bar shows the median; the whisker spans the fastest and slowest trial. A tool is marked fastest only when its lead over the next one is wider than either tool's own whisker; otherwise the card says so and names nobody.

The Cargo row means something different in each scenario, so the card tags it. In the commit scenario it is the uncached build CI does without a cache. In the edit scenario it is the incremental rebuild the caches have to keep up with. The warm scenario has no Cargo row, because with an empty target/ Cargo would repeat the build that warmed the store.

The scenarios

Warm build

A first build warms the store, then target/ is wiped and the same commit builds again. This is a runner restoring its cache and building something it has already seen.

Next commit

The store is warmed at one commit and the build runs at the next. Most of the dependency graph is unchanged and a few crates are not. Cargo's row is a cold build, since with an empty target/ that is all it can do.

Local edit

A full build, then one line of hk's own source changed and rebuilt in the same target/ with incremental compilation on. Almost nothing recompiles, so the cache's own bookkeeping is most of what shows up. Two details keep it honest:

  • CI is unset for every tool. mbx switches learned incremental reuse off when it sees that variable, on the reasoning that a fresh runner never edits code. With it set, every edit recompiled the crate in full.
  • The first edit after a build is discarded and the second is timed. Cargo's own build already wrote its incremental state, while mbx builds an edited crate's private state on the first edit and reuses it afterwards. The card shows what that first edit cost, since a developer waits for it once per fresh build.

Six parallel jobs

Six overlapping Rust CI jobs from an empty store: default and all-targets/all-features variants of cargo check, Clippy, and test compilation. The sequential row runs them in turn in one target/. The two parallel rows give each job its own target/, as separate CI steps would, and differ only in whether the machine-wide scheduler is on. Cargo bounds the compilers it starts itself and knows nothing about the Cargo process beside it; the scheduler gives every process one pool of permits and holds identical compilations until the first finishes. Peak compilers and lowest free memory show whether a faster batch shared the machine or oversubscribed it.

Keeping it fair

  • The registry is fetched once, before any timed build. No cell is timed while it downloads crates.
  • Every trial starts from a fresh clone, an empty store, and a new target/. Nothing carries over between tools or between runs.
  • The toolchain is pinned. hk does not pin one, and a runner-image Rust bump would change every cache key at once and look like a cache that stopped working.
  • CARGO_INCREMENTAL=0 matches CI everywhere except the edit scenario. Any inherited RUSTC_WRAPPER is cleared, and the run fails if the Cargo baseline turns out to be an mbx shim.
  • Both caches run local-only. A remote would measure the network.
  • Validity checks reject runs that do not exercise the intended cache behavior. That is any run where a warm build restored nothing or was no faster than the build that seeded it, where the edit rebuild compiled nothing, or where the scheduled contention batch went past its permits or the unscheduled one never did.

Running it yourself

sh
mise run bench

That builds mbx, clones hk, and runs the warm, commit, and edit scenarios once each. kache is included when it is on PATH and noted as skipped otherwise. mise run bench:refresh is what CI runs: every scenario, three trials each, written to benchmarks/results.json.

The bench-refresh workflow runs weekly, only when the published numbers were measured with an older mbx than the one on main, and opens a pull request rather than publishing directly.

What this does not measure

These results describe the pinned hk workload. A project with a very different dependency shape, such as heavy proc macros, a large C component, or many small leaf crates, will see different ratios. Three trials expose some variation; their ranges are not confidence intervals, and a “fastest” label is a display heuristic, not a statistical significance test. The benchmark is Linux-only, and limits covers what changes on macOS and Windows.

Instruction-counted measurements of mbx's own startup path, and cold and warm correctness runs against this workspace, live in benchmarks/.

MIT LicenseCopyright © 2026jdx.dev