Skip to content

Benchmarks ​

These benchmarks compare mbx with plain Cargo and kache on two pinned Rust projects, built with cargo build --locked. hk shows a change to one application crate; aube shows changes propagating through a workspace. Published results come from GitHub Actions. A tool is labeled fastest only when its lead exceeds the observed variation between trials.

hk: one application crate changes ​

jdx/hk is a mid-size Rust CLI with C dependencies. Its next-commit build recompiles one crate while restoring its dependencies. This is the workload featured in the landing-page showreel.

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
kache1.2s 1.1–1.2s
mbx1.3s 1.3–1.3s

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

mbx restored 1,665 output files on 354 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
mbx2.05× faster than Cargo9.2s 8.5–9.3s
kache1.97× faster than Cargo9.6s 8.6–9.9s
Cargouncached18.9s 18.3–19.1s

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

mbx restored 1,663 output files on 353 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
kachelevel with Cargo2.3s 2.2–2.7s
mbxlevel with Cargo2.4s 2.2–2.6s
Cargoincremental rebuild2.4s 2.3–2.5s

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

mbx restored 7 output files on 2 cache hits.

First edit after a build, before the loop settles: kache 9.3s, mbx 9.1s, Cargo 2.2s.

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
sequential32 compilers at peak, 62.5 GB free at the low50.7s 46.9–51.3s
parallelscheduler off162 compilers at peak, 53.3 GB free at the low48.2s 46.1–50.2s
parallelmbx schedulerfastest32 of 32 permits used, 61.6 GB free at the low39.2s 37.0–39.3s

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

Cache hits over the batch: sequential 49; parallel, scheduler off 12; parallel, mbx scheduler 3,402. 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-da58b12ru280a) with Rust 1.94.0, building hk at fc29ead under mbx 1.17.0, cargo 1.94.0 (85eff7c80 2026-01-15), rustc 1.94.0 (4a4ef493e 2026-03-02), kache 0.26.3. The run that produced them has the per-build logs.

aube: changes propagate through a workspace ​

jdx/aube is a 15-crate Rust workspace with C dependencies. Its next commit changes two libraries and rebuilds their dependents, exposing the work that whole-compilation caching cannot reuse.

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
mbx3.3s 2.9–4.1s
kache3.6s 3.5–4.1s

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

mbx restored 2,350 output files on 577 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
mbxfastest1.43× faster than Cargo23.2s 22.7–23.4s
kache1.30× faster than Cargo25.5s 25.5–26.4s
Cargouncached33.2s 33.1–33.4s

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

mbx restored 2,328 output files on 568 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 rebuildfastest7.9s 7.8–8.1s
mbx0.3s behind Cargo8.2s 8.2–8.3s
kache0.4s behind Cargo8.3s 8.3–8.5s

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

First edit after a build, before the loop settles: Cargo 8.0s, mbx 24.9s, kache 27.5s.

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
sequential32 compilers at peak, 60.1 GB free at the low2m 12s 132.1–133.1s
parallelscheduler off172 compilers at peak, 50.6 GB free at the low1m 7s 66.4–68.1s
parallelmbx schedulerfastest31 of 32 permits used, 54.9 GB free at the low54.7s 52.5–54.8s

With the scheduler on, the parallel batch finished 12.3s sooner than unscheduled and 77.4s sooner than running the jobs in turn, peaking at 31 compilers instead of 172.

Cache hits over the batch: sequential 3; parallel, scheduler off 23; parallel, mbx scheduler 3,550. 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-kv0r2drcad4f6) with Rust 1.94.0, building aube at e9a36eb under mbx 1.19.0, cargo 1.94.0 (85eff7c80 2026-01-15), rustc 1.94.0 (4a4ef493e 2026-03-02), kache 0.26.3. The run that produced them has the per-build logs.

Below each project's charts, a line names the mbx version, Rust version, and CI run that produced its results. Historical runs can use different mbx releases, so compare tools within one project's run. A refresh measures both projects with the same mbx release.

Read the results ​

Each scenario has three independent trials per tool. A trial starts with a fresh clone and empty store, then performs the scenario's warm-up and measured builds. The bar shows the median; the whisker spans the fastest and slowest trial. A tool is marked fastest only when its lead exceeds both tools' trial ranges. Otherwise, the card reports no clear winner.

The Cargo row means different things in different scenarios, so the card tags it. In the new-worktree and commit scenarios it is the uncached baseline: the cold build a fresh checkout or CI runner 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 six-parallel-jobs scenario has no Cargo row either, because every row there runs mbx.

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.

New worktree ​

A build at one commit warms the local store. Then the same commit builds in a new Git worktree with no target directory of its own. Each tool uses its default target placement, and the benchmark harness does not copy the first worktree's target directory. The scenario tests reuse across checkout paths, including dependency target seeding when the tool and Cargo support it. Cargo's cold build provides the uncached baseline. Start new checkouts from existing units describes how mbx seeds a new checkout.

Results published before this scenario was added do not include it, so a project whose current run predates it has no worktree card. The line under each project's charts names the run that produced its cards.

Next commit ​

The store is warmed at one commit and the build runs at the next. hk's next commit changes its application crate. For aube, the next commit changes two library crates, so they and the workspace crates that depend on them compile again while the rest of the graph is restored. Cargo's row is a cold build, since with an empty target/ that is all it can do.

Local edit ​

A full build runs first. Then one line changes in hk's src/main.rs or in aube's aube-util, which most of aube's workspace depends on, and the project rebuilds in the same target/ with CARGO_INCREMENTAL=1. mbx overrides that, as it does by default, and uses learned incremental reuse instead. This measures the edit/build loop, including cache bookkeeping and incremental compilation. Two details make the comparison useful:

  • The harness unsets CI for every tool. mbx switches learned incremental reuse off in CI because fresh runners have no earlier edit state to reuse.
  • The first edit establishes incremental state; the second supplies the headline timing. Cargo's full build already wrote its incremental state, while mbx builds an edited crate's learned incremental state on the first edit and reuses it afterwards. The card also shows what that first edit cost, since you wait 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. Every row runs mbx; this scenario compares mbx with itself and has no Cargo or kache row. The sequential row runs the jobs in turn in one target/ with the scheduler on. 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.

Fairness controls ​

  • The registry is fetched once, before any timed build, so no timed build includes crate downloads.
  • Every trial starts from a fresh clone, an empty store, and a new target/. Nothing carries over between tools or between runs.
  • Like your own build, the harness uses whichever Rust toolchain the runner provides. The toolchain field in each results file records which release produced the numbers. Every trial in a run uses that one compiler, so the tools are compared fairly against each other. A Rust upgrade between runs changes every cache key at once, which reads as a cache that stopped working. Check that field before treating a drop across runs as a regression.
  • The harness sets CARGO_INCREMENTAL=0, as CI does, in every scenario except the edit scenario. It clears any inherited RUSTC_WRAPPER or RUSTC_WORKSPACE_WRAPPER, and the run fails if the Cargo baseline turns out to run under mbx, for example through the Cargo shim mbx setup installs.
  • Both caches run local-only, since a remote would measure the network. The harness runs kache with KACHE_LOCAL_ONLY=1. It does not clear an mbx remote cache; published results are local-only because the CI runner that produces them configures none.
  • The harness rejects a run that does not exercise the intended cache behavior. A run fails when:
    • a warm build restored nothing, or ran no faster than the build that seeded it
    • mbx wrote no build report (MBX_STATS_REPORT) for a new-worktree build
    • an edit rebuild compiled nothing
    • the scheduled contention batch went past its permits, or the unscheduled one never did

Run the benchmarks yourself ​

sh
mise run bench

That builds mbx, clones aube, and runs the warm, new-worktree, commit, and edit scenarios once each. kache is included when it is on PATH and noted as skipped otherwise.

The harness does not turn off an mbx remote cache, so with one configured the mbx rows include network restores. Set MBX_REMOTE_URL to an empty value for the run; it overrides remote.url in your configuration file:

sh
MBX_REMOTE_URL= mise run bench

mise run bench:refresh runs CI's full measurement against the mbx built from your checkout: both projects across every scenario, three trials each, written to benchmarks/results.json (hk) and benchmarks/results-aube.json (aube).

CI runs the same harness against the latest release instead. The bench-refresh workflow checks weekly and reruns when the recorded mbx version differs from the latest release. It measures that released version and opens a pull request with the results. On main, a manual run with force also opens or updates that PR, even when the recorded version already matches. Set dry_run to measure without proposing an update, or source to measure the selected source ref instead of the latest release; source implies dry_run.

What this does not measure ​

These results describe the two pinned workloads. A project with a 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 Caching 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