NativeLink
Understanding NativeLink

Local Remote Execution

Hermetic, content-addressed builds on your machine. No Docker, no containers, no surprise dependencies.

Local Remote Execution (LRE) is NativeLink's answer to the question every team eventually asks: how do I get the hermeticity of remote execution without losing the iteration speed of building locally?

The short version: run a remote-execution worker on your laptop, pin the toolchain with Nix, route every action through the worker. Same guarantees as RBE; no network round-trip.

What LRE gives you

  • Identical artifacts on every machine. The Nix-pinned toolchain produces bit-identical output across your laptop, your colleague's laptop, and CI.
  • Real hermeticity, no Docker. No container runtime overhead, no image rebuild on every toolchain change.
  • Cache sharing with remote workers. Because the action hashes match across local and remote execution, a cache hit one place is a cache hit everywhere.
  • Works offline. Once the toolchain is fetched, LRE is a pure local computation.

How it works

Three pieces in collaboration:

  1. A NativeLink worker running on the developer's machine, bound to the loopback interface.
  2. A pinned toolchain, provided by a Nix flake. Every binary the build invokes — compiler, linker, assembler, even coreutils — is content-addressed in the Nix store.
  3. A configured client (Bazel, Buck2, ...) that targets the local worker the same way it would target a remote cluster.

When the client issues an Execute call, the action's inputs are fetched from the local CAS (also running on localhost), the worker runs the command in a sandbox using the pinned toolchain, and the outputs are stored back in the CAS — same protocol path as remote execution, with the network replaced by a Unix socket.

When to use LRE

ScenarioUse LRE?
Solo dev iterating on a personal monorepo
Team of 5 sharing a cache without a server
Reproducing a CI failure locally
Fan-out across hundreds of cores✗ (use a remote worker fleet)
Building for a platform you can't host

The split is "is the bottleneck CPU or determinism?" — LRE gives you determinism. For raw throughput, you still want a remote worker pool.

Setup

The recommended flow:

  1. Install Nix with flakes enabled. The next-gen installer is the easiest path.

  2. Pull the NativeLink Bazel flake template.

    nix flake init -t github:TraceMachina/nativelink#bazel
  3. Enter the dev shell. This downloads the Nix-pinned toolchain on first run and generates lre.bazelrc, which the template's .bazelrc already try-imports.

    nix develop
  4. Point user.bazelrc at a NativeLink cache and executor. The template ships it with placeholders:

    build --remote_cache=grpcs://TODO
    build --bes_backend=grpcs://TODO
    build --remote_timeout=600
    build --remote_executor=grpcs://TODO

    Fill these in with either your dev.nativelink.com credentials or a self-hosted cluster with a worker capable of the platform the example needs. A plain local instance (drop the s in grpcs://, point at 127.0.0.1) is enough to validate remote_cache, but the C++ example's lre-cc platform still needs a real worker running the matching container image — see Local Remote Execution for the exact commands and what each one actually requires.

  5. Build the example.

    bazel build hello-world

The first build will be the same wall-time as a normal local build. The second one will be near-instant — that's the cache doing its job.

Adding LRE to an existing flake-parts project

The template above is the fastest way to see LRE working, but it starts a new project. To add the same Nix-pinned toolchains to a project you already have, see the full flake-side and Bazel-side wiring — the nativelink.flakeModule import, lre.installationScript, and the local-remote-execution Bazel module override — in local-remote-execution/README.md.

Why Nix, specifically?

LRE needs every input to a build action to be hashable. The compiler, the linker, the standard library headers, the bash that runs the wrapper script. Hashing a /nix/store/...-clang-18 path is trivial because the path itself encodes a hash of every input that produced it. Hashing a /usr/bin/clang is impossible without scraping its filesystem state.

That's the entire reason. Nix gives us hash-pinned tools; everything else follows.

For background, see What is Nix? and How do I make my Bazel setup hermetic?.

What's next

On this page