Skip to content

How it works

mbx works at the compiler boundary. Cargo decides which tools need to run; mbx decides whether each eligible invocation can be restored from cached work. An action is one modeled invocation and its inputs. The content-addressed store (CAS) holds outputs under digests of their content.

Run mbx build directly, or use ordinary cargo build after mbx setup. Both follow the same build lifecycle.

From command to result

  1. mbx resolves the workspace and target roots through Cargo metadata.
  2. It starts an in-process cache agent and creates shims for the build.
  3. Cargo runs normally with the rustc shim set as RUSTC_WRAPPER, and build scripts inherit HOST_CC and HOST_CXX pointing at the C and C++ shims.
  4. Each shim analyzes its compiler invocation and derives a content-addressed action key.
  5. A hit restores the action's outputs; a miss runs the real compiler and publishes the result.
  6. The agent exits with the build, draining any remote uploads it still owes. There is no persistent daemon.

The installed Cargo shim and the zero-config mbx <cargo-command> form both work this way. The installed shim follows the active mbx when mbx is upgraded.

That wrapper boundary also covers multiple Cargo builds running at the same time. Their compiler shims share a machine-wide permit pool and an in-flight-work registry, so those builds do not multiply the machine's CPU and memory budgets or repeat an identical cold compilation. Machine-wide scheduling below describes the mechanism, and the mise task example and parallel GitHub Actions example are ready-to-use recipes.

Build-script C and C++

Cargo has no CC_WRAPPER, so the shims arrive as compiler variables themselves, resolved to the platform compilers when the session starts. They are set as HOST_CC and HOST_CXX rather than CC and CXX: the cc crate consults the host pair only when it is not cross-compiling, and these shims wrap the host compiler, so a cargo build --target keeps the cross compiler it would have found on its own. An explicit host compiler in CC, CXX, HOST_CC, or HOST_CXX is left alone. Explicit target compilers can be wrapped; see the C and C++ limits. MBX_CC=0 turns this caching off.

Unlike rustc, a C compile leaves no dependency record behind for a later build to read, and publishing one would add a file the uncached build never produced. So the shim asks for its own dependency list, keeps it private, and keys the compilation on the files that list names. A cold compilation therefore has no key to look up yet; it is stored after compiling and warms the next build. A build that asks the compiler for its own list, as OpenSSL's makefiles and CMake do with -MD or -MMD, gets one from the shim instead, written from the same files whether the object was compiled or restored; the flags shape that list and nothing else, so they are not part of the key. The directories the compile searched also contribute a manifest of the names in them that could answer an #include, so a header appearing where it would shadow one that was read changes the key even though every file that was read is unchanged. What a manifest counts and leaves out is covered in limits.

Portable keys

Known workspace, target, Cargo registry, toolchain, and sysroot paths are mapped to stable placeholders before they enter a key. That is what lets equivalent worktrees share an action even though their absolute paths differ.

The key also covers compiler inputs and relevant environment. If mbx cannot model something exactly, it bypasses the action.

Prediction and dep-info

A rustc action key depends on the files that compilation actually reads. mbx learns that set from Cargo/rustc dep-info left by an earlier build and records a prediction for later invocations. A cold compilation may therefore have no key to look up yet. It still gets stored after compiling and can warm the next build.

Predictions are filed under the digest of Cargo.lock, so a dependency bump starts a new record. A build whose lockfile has no record yet borrows the one made for the lockfile before it, found through Git's history of the file, up to eight states back, and failing that the newest records in the store, which on a runner that restored a cache bundle are the builds that produced it. A bump leaves most of the graph unchanged, and those predictions still hash to results the cache holds; the crates the bump touched miss and are recorded afresh. The borrowed record is kept under the new lockfile, so the commands that follow, tests and lints included, start from it as well, and a trusted build publishes it there. A shallow clone offers only the history it fetched: a pull request checkout with fetch-depth: 2 reaches its base branch's lockfile, and a fetch-depth: 1 checkout relies on the store.

Hashing those files is shared too. The agent keeps a ledger of every file a shim has hashed, keyed by the file's length, modification time, and change time, so a dependency's rlib is read once however many crates link it. The ledger is saved with the checkout's private state when the build finishes and loaded by its next build, avoiding repeated reads of unchanged dependencies. Reuse depends on the file-identity checks supported by that filesystem; network filesystems receive additional content validation. If an entry cannot be validated, mbx hashes the file again.

Rustdoc actions

Cargo invokes rustdoc through a separate shim. Each documentation action keys the rustdoc version and arguments, the package source tree, explicit compiler artifacts, and Cargo's compile-time environment. Rustdoc's mergeable-output mode separates deterministic per-crate pages from files such as the search index that combine every documented crate. mbx caches the former with the crate's merge metadata and runs rustdoc's inexpensive finalization step after restoring them, so cached dependency documentation remains composable and the shared indexes do not depend on restore order.

Copy-on-write output restoration

The cache agent verifies each local CAS blob against its digest before returning it to the rustc wrapper. The wrapper first tries to reflink that verified blob into a staging directory beside Cargo's destination, applies the expected file mode, and atomically renames it into place. A reflink is an ordinary file that shares its data blocks with the CAS until either copy is written, so Cargo sees the complete output immediately without the wrapper re-reading or allocating all of its data after verification. Writes to a restored output cannot change the CAS object.

Reflinks require support from the filesystem and generally require the cache and target directory to be on the same filesystem. When cloning is unavailable, mbx copies the bytes instead. The session summary reports the file count and logical size handled by each path; MBX_STATS_REPORT includes the same values as reflinked_output_files, reflinked_output_bytes, copied_output_files, and copied_output_bytes.

This is filesystem copy-on-write, not a placeholder or userspace on-demand filesystem. Restored paths retain normal file semantics on every supported platform, including when mbx has to use the copy fallback.

Machine-wide scheduling

Every compiler mbx starts takes a permit from one pool shared by every build on the machine, so simultaneous builds do not multiply the machine's CPU and memory budgets. Cache hits never wait, Cargo keeps its own dependency scheduling, and permits are released by the kernel if a process dies, so a crashed build cannot wedge its siblings.

Concurrent builds also stop repeating each other. Four CI jobs building one commit compile the same dependency graph four times; under the scheduler, a compilation identical to one already running anywhere on the machine waits for that one to finish and restores its result from the cache. The finished compilation also leaves its input list behind, so a job arriving after it is already done can build the cache key it would otherwise lack and hit where it would have compiled cold. Both paths rehash every input before trusting anything, so the worst a stale record can do is fall back to compiling.

Permits are weighted by memory. Native links start at two permits, and every compilation is thereafter weighted by what it actually used, so the predicted memory of everything running stays inside the budget. A link that turns out to fit in one permit stops being charged for two, which keeps the link-heavy tail of a build from running at half concurrency. A link mbx has never seen is weighed by the heaviest of this machine's recent links; test binaries each have their own crate name, so a cold cargo test --no-run has no per-crate history for the links in front of it. A compilation the Linux OOM killer stops is recorded heavier than it measured, so its retry runs with more room.

The pool size, memory budget, and priority are settings; see machine-wide compile scheduling.

What remains local

Cargo's target state belongs to a workspace. Learned incremental state belongs to a checkout and never enters the shared cache. A configured remote receives eligible shared actions according to the write policy. Managed targets and garbage collection control local retention.

Correctness first

Unsupported crate types, unmodeled search paths, and incremental compilations bypass the shared action cache. That includes the private incremental state of learned incremental reuse, which is never published. A compilation that links nothing is cached whatever its crate type: cargo check and clippy compile every binary and test target that way. A native link is admitted only when its linker can be described: host binaries, tests, and proc macros on Linux, macOS, and Windows, where mbx puts the resolved linker, startup objects, C runtime, and SDK into the key, and a fixed allowlist of built-in WebAssembly targets whose default linker and system inputs ship with rustc. Everything else (native libraries, custom linkers, unrecognized toolchains) links as it always did; see limits. MBX_VERIFY=1 compiles while also consulting the cache and compares the result, an expensive qualification mode.

MIT LicenseCopyright © 2026jdx.dev