Bazel Remote Execution
Run Bazel actions on Namespace compute, with remote workers started on demand for your build.
Remote Execution is in early access. Reach out to enable it for your workspace.
How it works
A Bazel Remote Execution cluster has three components:
- Scheduler: accepts Bazel Remote Execution API requests and assigns actions to workers.
- Storage: stores content-addressable storage (CAS) entries durably across the cluster.
- Workers: start on demand and execute Bazel actions.
Each worker advertises a fixed number of execution slots. A slot represents one unit of concurrent work, so a worker with four slots can run up to four actions at the same time. Namespace starts workers as your build needs capacity and routes actions to workers that match the requested execution platform.
Bazel's remote execution protocol supports multi-platform builds. For example, you can run Bazel from a Linux host while selected actions execute on macOS workers. This is useful for repositories that need to produce or test artifacts on multiple operating systems without moving the entire CI job to each platform.
Getting started
First, make sure your workspace has early-access RBE enabled. Then use the CLI to create a Bazel configuration file for the execution cluster:
Configure execution access
$nsc bazel execution setup --bazelrc=~/.namespace.bazelrc
This command provisions the scheduler and storage components if needed and writes a Bazel configuration file with the remote executor, remote cache, credentials, and recommended execution defaults.
Run Bazel with remote execution
$bazel --bazelrc=~/.namespace.bazelrc build //...
You can pass multiple --bazelrc flags if you need to combine Namespace's
generated configuration with your existing project configuration.
For a quick early-access smoke test, generate a configuration file:
$ nsc bazel execution setup --bazelrc=~/.namespace.bazelrc --key=docs-smoke-testIf the command succeeds, Namespace provisioned the execution cluster and wrote a Bazel configuration file that points at it.
Example: Linux host to Linux workers
This example runs Bazel from a Linux host and lets Namespace execute actions on Linux workers. It uses a public Bazel repository so you can try the flow without changing your own project first.
$ git clone https://github.com/bazelbuild/examples.git
$ cd examples/cpp-tutorial/stage3
$ nsc bazel execution setup --bazelrc=~/.namespace.bazelrc --key=examples-linux
$ bazel --bazelrc=~/.namespace.bazelrc build //...By default, actions without explicit platform properties are routed to Linux workers.
Example: Linux host to macOS workers
Bazel can also route selected actions to macOS workers from a Linux client. The linux-to-mac-rbe-demo repository demonstrates that setup.
This example requires macos/arm64 workers to be enabled for your workspace. If
you see an error such as no on-demand worker capacity configured for platform "macos/arm64", reach out to the Namespace team to enable macOS worker capacity.
$ git clone https://github.com/namespacelabs/linux-to-mac-rbe-demo.git
$ cd linux-to-mac-rbe-demo
$ nsc bazel execution setup --bazelrc=~/.namespace.bazelrc --key=linux-to-mac-demo
$ bazelisk --bazelrc=~/.namespace.bazelrc build //DemoApp:DemoAppFor your own targets, route an action to macOS by setting Bazel execution properties on that target or platform, for example:
exec_properties = {
"OSFamily": "macos",
"Arch": "arm64",
}Actions without macOS execution properties continue to run on the default Linux workers, allowing one Bazel invocation to use both platforms.
Custom worker base images
By default, actions run inside Namespace's standard Linux worker image. If your actions need extra tools, libraries, or a specific base environment, you can run them inside your own container image instead.
Custom images are selected per action through the standard Bazel
container-image execution property. The scheduler routes actions to workers
started with the requested image, and actions without a container-image
property continue to run on the default worker.
A few requirements apply:
- The image reference must be pinned to an immutable digest
(
repo@sha256:...). Mutable tags are rejected, because a tag could move to a different image while a warm worker for the old digest is still pooled. - The image must be optimized before it can be used. Namespace converts the image into a fast-booting disk variant; the scheduler rejects any custom image that has not been optimized yet (see below).
- The worker must be able to pull the OCI image and therefore it has to be available publicly. Pushing it to your tenant's
nscr.ioregistry is the simplest option. - Custom images are supported on Linux workers only and they won't work on macOS workers.
Build, push, and optimize a custom image
A custom worker image starts as an ordinary OCI image. After you push it, you pin it to a digest and optimize it once
Build and push your image
Write a Dockerfile that starts from any Linux base and adds the tools your
actions need. Authenticate against nscr.io once with
nsc docker login, then build and push it
with nsc build for the worker architecture
(linux/amd64 or linux/arm64):
$ nsc docker login
$ nsc build . -t nscr.io/<tenant>/my-worker:latest --platform linux/amd64 --pushAlready have an image elsewhere? Use
nsc base-image upload to pull it and
push it into your nscr.io registry instead:
$ nsc base-image upload <source-image> my-worker:latestPin it to an immutable digest
The scheduler requires a digest, not a tag. Resolve the digest your push produced:
$ docker inspect --format '{{index .RepoDigests 0}}' nscr.io/<tenant>/my-worker:latest
nscr.io/<tenant>/my-worker@sha256:<digest>Use that repo@sha256:<digest> reference everywhere below.
Optimize the image
Optimize the pinned image with
nsc base-image optimize so workers
can boot from it. This step blocks until the optimized variant is ready:
$nsc base-image optimize --image_ref nscr.io/<tenant>/my-worker@sha256:<digest>
You only need to optimize a given digest once. Pushing a new image with a new digest requires optimizing that new digest.
Optimizing a base image from a GitHub Actions job requires the job to run with additional permission grants. Pass the baseimage:*:* grant as a permissions.additional_grant feature setting on the runner.
With a runner profile, append it to the profile name:
jobs:
rbe:
runs-on:
- namespace-profile-foobar;permissions.additional_grant=baseimage:*:*With a machine label, pass it through the namespace-features: label instead:
jobs:
rbe:
runs-on:
- nscloud-ubuntu-22.04-amd64-4x8-with-features
- namespace-features:permissions.additional_grant=baseimage:*:*See Configuring Features and Overrides for the full syntax of feature settings, and Access Level for restricting what a runner can do.
This grant is only needed when optimizing from a GitHub Actions job. Running
nsc base-image optimize from a local machine outside of Namespace does not
require it.
Use it in your build
Point your actions at the optimized, digest-pinned image with the
container-image execution property (see the next sections).
If you run a build against an image that has not been optimized yet, the scheduler rejects the affected actions with an INVALID_ARGUMENT error telling you the image is not optimized. Run nsc base-image optimize for that digest and retry.
Set a custom image for the whole build
Use --remote_default_exec_properties to apply one image to every action that
does not override it:
$ bazel --bazelrc=~/.namespace.bazelrc build //... \
--remote_default_exec_properties=container-image=docker://<repo>@sha256:<digest>Set a custom image per target or platform
Set the container-image execution property on a specific target or platform so
only those actions run inside the custom image:
exec_properties = {
"container-image": "docker://<repo>@sha256:<digest>",
}The docker:// scheme prefix is optional.
Run preparation scripts at worker startup
A custom worker image can ship preparation scripts that run once before the
worker starts handling any actions. These hooks are for setup related actions that
must happen on every worker boot, such as mounting an external /nix store or
seeding caches.
Place executable files in /etc/namespace/prepare.d inside your image. On
startup, the worker runs every executable file in that directory in lexical
order (e.g. 10-mount.sh before 20-warmup.sh). Non-executable files and
subdirectories are skipped. Scripts inherit the worker agent's environment
and stream their output to the worker logs.
Preparation scripts are critical: they run at the very start, and if any script exits non-zero the worker fails to start and handles no work. Make sure your scripts are reliable and idempotent.
Using revocable tokens
Authenticating Remote Execution with a revocable token requires nsc v0.0.528 or later. Run nsc version ensure --at_least 0.0.528 to check and update the CLI if needed.
By default, nsc bazel execution setup uses your interactive login and writes a
bazelrc that authenticates to the cluster with a short-lived mTLS client
certificate. For CI/CD pipelines, automation, or any environment without an
interactive login, you can instead authenticate with a
revocable token.
Revocable tokens are long-lived but can be revoked at any time from
cloud.namespace.so/user/sessions or
with nsc token revoke.
Create a revocable token
Scope the token so it can provision the execution cluster and reach its endpoints, and write it to a file:
$ nsc token create \
--name "rbe-ci-token" \
--grant '{"resource_type":"bazel/execution","resource_id":"*","actions":["ensure"]}' \
--grant '{"resource_type":"ingress","resource_id":"*","actions":["access"]}' \
--token_file rbe-token.jsonGenerate a bazelrc with the token
Point NSC_TOKEN_FILE at the token file and pass --static. This provisions a
cluster that uses the revocable token to obtain short lived credentials:
$ export NSC_TOKEN_FILE="$(pwd)/rbe-token.json"
$ nsc bazel execution setup --static --bazelrc=~/.namespace.bazelrc --key=rbe-revocableRun Bazel with remote execution
$ bazel --bazelrc=~/.namespace.bazelrc build //...Current caveats
Remote Execution is still early and the experience is actively changing:
- Worker sizing is not yet self-service. Reach out to the Namespace team to change worker shapes or slot counts.
- Action observability is still missing.
- Actions are not sandboxed from each other when running in the same worker, but workers are never shared across workspaces.
- Action input hydration and result upload performance still needs further work.
If any of these caveats block your use case, contact support@namespace.so so we can help choose the right rollout path.

