Sharif Haason
Rust · Build systems

Making Cargo
faster.

How Buck2 and Bazel reuse work, where Cargo spends time, and what I would change in its execution engine.

01

What a build does

I want Cargo’s commands and package management with the execution and reuse model of a large build system. To evaluate that idea, first separate the build system from the compiler.

Cargo resolves dependencies, selects features and targets, decides which work is stale, and schedules compiler invocations. rustc checks the Rust code and generates machine code. The linker combines compiled code into the final executable. Build scripts and procedural macros add their own work.

FIG. 01The main responsibilities in a Rust build. Cargo schedules many compiler invocations; rustc invokes the linker for outputs that require linking.

There are three distinct ways to reduce the wait: run each operation faster, avoid operations, or overlap independent operations. Compiler optimization addresses the first. Caching and precise change tracking address the second. Scheduling and remote execution address the third.

A build system still has to respect dependencies. If one slow crate blocks every downstream crate, adding workers leaves that chain intact. Cargo’s timing report exposes this critical path.[1]

02

Cargo already skips and overlaps work

Cargo has a dependency graph, freshness checks and parallel scheduling. It also pipelines compilation: a dependent crate starts after the metadata it needs becomes available, while the dependency continues generating code. Cargo and rustc coordinate concurrency through a jobserver.[1]

Cargo tracks build units using fingerprints, dependency information and filesystem state. These records live alongside build outputs. They let Cargo reuse local results when the relevant inputs remain unchanged. That local freshness model is distinct from a portable action cache shared by multiple machines.[2]

Build scripts make the boundary harder. A build.rs program runs arbitrary build logic: generating Rust, compiling C, locating libraries or probing the host. It reports file and environment dependencies through rerun-if-changed and rerun-if-env-changed. Without file-change directives, Cargo checks the package broadly for changes.[3]

// Declare the actual inputs to code generation.
println!("cargo::rerun-if-changed=schema.proto");
println!("cargo::rerun-if-env-changed=PROTOC");

These declarations help Cargo decide when to rerun a script. A reusable remote action also needs an exact identity for the compiler, generator, native libraries, arguments and environment. Every input that changes the result belongs in that identity.

03

How Buck2 and Bazel save work

Explicit actions make reuse portable

An action is one build step with declared inputs, outputs, a command and an environment. Bazel separates the action cache, which maps action identities to results, from the content-addressed store, which holds output bytes. Another developer or CI job requests the same action and fetches its existing result.[4]

FIG. 02A shared action cache avoids recomputation. On a miss, the executor runs the action and records the result. This is a conceptual flow, independent of a specific hash algorithm.

The cache key must describe the computation. If two different compilers share a key, the cache returns the wrong result. If an irrelevant machine path changes the key, reuse disappears. Missing inputs break correctness; unnecessary inputs reduce cache hits.

Hermetic builds control hidden inputs

A hermetic action uses identified tools and dependencies inside a controlled environment. It does not silently depend on whichever library or compiler happens to be installed on the host. Sandboxing restricts access; declared inputs and reproducible tools establish what the action means.[5]

Persistent graphs reduce repeated analysis

Buck2’s daemon retains build state between commands. Its DICE engine tracks dependencies between computations and invalidates affected results as inputs change. Buck2 puts loading, analysis and execution dependencies into one incremental graph rather than enforcing a global barrier between each phase.[6][7][8]

That is separate from rustc’s internal incrementality. The build engine reuses graph analysis and completed actions. The compiler reuses work inside a compilation. A design needs to account for both.

Remote execution adds compute capacity

Remote execution sends eligible actions to workers. It helps when the graph exposes enough independent work to use those workers. Its costs include transferring inputs, waiting in queues and fetching outputs. A cache hit reuses an existing result; remote execution computes a missing one.[9]

04

Which builds get faster

A clean build, a no-op build and an edit loop exercise different systems. Compare them separately.

WorkloadMain costUseful intervention
Clean build, empty cacheCompile and link the required graph.Faster compiler and linker; shorten the critical path; run independent actions concurrently.
No source changesEstablish that existing outputs are still valid.Cheap freshness checks and retained graph analysis.
Small local editRecompute affected compiler work and downstream outputs.Compiler incrementality, precise invalidation and less relinking.
Fresh checkout, populated shared cacheIdentify reusable actions and fetch their outputs.Stable action keys and fast cache access.
Large graph, remote workersExecute cache misses across available workers.Declared inputs, parallel actions and low queue and transfer overhead.

A remote cache saves time when lookup and transfer cost less than recomputing the result. Remote workers save time when the extra parallelism exceeds dispatch and transfer costs. Neither condition follows from the name of the build tool.

Also keep build speed separate from program speed. Reducing optimization or debug information changes the work performed and the artifact produced. A fast cargo check does not measure executable generation. A faster executable does not prove a faster build.

05 · Proposed design

The Cargo execution engine I would test

I would keep Cargo.toml, Cargo.lock, feature resolution and the normal commands as the frontend. Behind that interface, I would test a persistent graph and an explicit action model. This is a design proposal, not an implemented Cargo fork.

FIG. 03Proposed architecture. The frontend preserves Cargo semantics. The engine tracks dependencies, checks the cache and schedules misses. Remote execution is optional.
  1. Retain graph analysis across commands.

    Track manifests, configuration, sources and toolchain changes. Recompute affected graph nodes. Verify cold startup and daemon restart behavior alongside warm performance.

  2. Give every reusable action a complete identity.

    Include content digests, compiler and linker versions, target, profile, features, arguments and declared environment. Normalize paths without collapsing inputs that produce different results.

  3. Make build-script dependencies explicit.

    Track generators, native libraries, files and environment. Keep actions with unresolved host dependencies local and outside the shared cache. Expose the reason to the developer.

  4. Preserve useful compiler state.

    Measure local incremental compilation alongside action reuse. Treat compiler state and whole-action outputs as different cache layers. Keep their validity rules separate.

  5. Schedule against CPU and memory budgets.

    Account for heavy compiler and linker processes. Preserve Cargo and rustc’s coordination. Add remote workers only after the local action model produces correct results.

  6. Explain every rebuild and cache decision.

    Report the changed input, cache miss, transfer time and critical-path wait. Make the performance model inspectable through ordinary Cargo commands.

The compatibility work includes cross-compilation, host tools, native dependencies, procedural macros, tests, doctests, examples, diagnostics, IDE metadata and publishing. Passing a simple cargo build fixture covers only one part of that contract.

I would start with a bounded prototype that exposes action descriptions and rebuild reasons for one representative workspace. Then test cross-checkout reuse. A full fork comes after those results establish which Cargo boundaries need to change.

06

What already exists

Cargo plus a compiler cache

sccache wraps eligible rustc invocations through RUSTC_WRAPPER. It preserves Cargo’s workflow, but it does not cache the entire Cargo graph. Its Rust support excludes incremental compilations and crates that invoke the system linker, including binaries and procedural-macro crates. Compare it against normal local incrementality instead of assuming the two gains add together.[10]

Cargo metadata plus another build backend

Buck2 uses Reindeer to generate BUCK definitions for Rust dependencies. Bazel’s crate_universe integrates Cargo manifests and lockfiles into its dependency setup. These are existing routes for carrying Cargo package metadata into another execution model.[11][12]

Generated definitions still need to match the product’s features, build scripts, platforms and tests. The migration cost is maintaining those semantics across both workflows.

Cargo and rustc improvements upstream

The Rust fast-build roadmap includes compiler parallelism, faster development backends, incremental-system changes, downstream rebuild avoidance, linking and better Cargo/rustc coordination. These address different portions of the wait. Roadmap entries describe planned work and experiments; they are not a benchmark of a finished toolchain.[13]

07

The benchmark that answers the question

I would compare ordinary Cargo, Cargo with sccache, and a matched Buck2 or Bazel build of the same workspace. Use the same source revision, lockfile, compiler, target, features, profile, linker, native tools and selected outputs.

First verify behavior, diagnostics and representative tests. Then run these conditions in separate output and cache directories:

  1. Clean build: empty outputs and action cache, with dependency downloads excluded.
  2. No-op build: repeat the command without changing sources.
  3. Leaf edit: apply and restore the same function-body change.
  4. API edit: apply and restore the same public-interface change.
  5. Shared-cache reuse: start a fresh checkout against a populated cache and include transfer time.
  6. Remote execution: record worker capacity, queue time, transfer time and cache state.

Run check, debug build, test compilation and release build separately. Alternate run order, repeat each condition at least five times, and report the median and spread. Record compiler invocations, cache hits, graph-analysis time, critical path, CPU use, peak memory and artifact size.

Separate warm daemon state from cold startup. Record machine load and filesystem-cache conditions. Include graph generation and configuration costs. A faster run with different outputs or a prefilled cache answers a different question.

The decision rule

Use an existing cache or backend when it removes the measured cost. Propose an upstream change when the fix fits Cargo’s architecture. Build a fork when the required execution model crosses that boundary and the measured gain justifies maintaining it.

08

Public compiler work and reported results

In his October 20, 2026 thread, Charlie Marsh describes an Astral fork of rustc, Cargo, rustfmt and Clippy aimed at development speed while retaining correctness and borrow checking. He reports up to 30× for some incremental cargo check cases and 30–40% overall speedup when replaying development sessions. He says smaller patches were upstreamed and announces an intention to open-source the fork.[14]

Those are author-reported results for specific workloads. They are not a Cargo-versus-Buck2 benchmark. The thread does not establish the fork’s present release status.

Separately, the accepted Rust compiler-performance goal publicly lists OpenAI, Google, Devolutions and Fulfil as partial funders of Nicholas Nethercote’s profiling and optimization work. That goal targets rustc itself. It is distinct from the Astral fork.[15]

My conclusion is to preserve Cargo’s interface and test a different execution model behind it. The architectural targets are persistent analysis, complete action identities, cross-checkout reuse and observable scheduling. The benchmark determines their value.

References

Sources

Primary documentation and author statements, checked 8 November 2026. The architecture in section 5 and benchmark in section 7 are proposals. This article contains no new build-performance measurements.

  1. Cargo: reporting build timingsCompiler units, pipelining, critical paths and the jobserver.
  2. Cargo internals: fingerprintsFreshness, dependency information and build-script tracking.
  3. Cargo: build scriptsExecution, change declarations and host/target behavior.
  4. Bazel: remote cachingAction identities, result metadata and content-addressed outputs.
  5. Bazel: hermeticityTool identity, isolation and reproducible inputs.
  6. Buck2: the daemonRetained state between commands.
  7. Buck2: Modern DICEIncremental computation and dependency tracking.
  8. Why Buck2A unified incremental graph and dynamic dependencies.
  9. Bazel: remote executionExecuting build actions on remote workers.
  10. sccache: Rust supportEligibility, incrementality and linker limitations.
  11. Bootstrapping Buck2Cargo builds and Reindeer-generated Rust dependency definitions.
  12. rules_rust: crate_universeCargo manifest and lockfile integration.
  13. Rust Project Goals: Fast BuildsCompiler, linker and build-coordination roadmap.
  14. Charlie Marsh: compiler iteration-speed threadOriginal X thread; author-reported development-session results.
  15. Rust Project Goals: compiler performance optimizationsAccepted scope and public funding table.