Configuring Nx Remote Caching
Nx's local cache makes the second run of a task on your machine instant. A remote cache extends that across machines: when CI builds @acme/ui for one pull request, the next pull request — and every developer who pulls main — replays the result instead of rebuilding. Nx supports two routes: Nx Cloud, the hosted service, and self-hosted caches backed by object storage or your own HTTP server. This guide sets up both, explains the trust and security model, and shows how to measure whether the cache is actually paying off.
How remote caching works in Nx
For every cacheable task, Nx computes a hash from the task's inputs (files, dependency versions, environment and runtime inputs). Before running, it checks the local cache for that hash, then the remote cache. On a hit, it restores the task's outputs and replays its terminal output. On a miss, it runs the task and uploads the result. Accurate hashes are the foundation, so Configuring Nx Named Inputs for Accurate Caching is a prerequisite for trusting a shared cache.
Option 1: Nx Cloud
The fastest setup is Nx Cloud:
pnpm nx connect
The command creates a workspace on Nx Cloud and writes an nxCloudId into nx.json. From then on, cacheable tasks read from and write to the hosted cache. In CI, authenticate with an access token stored as a secret:
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
Nx Cloud offers more than caching — distributed task execution across agents, flaky task detection and run analytics — which is the main reason teams choose it. Access tokens can be read-only or read-write; give developer machines read-only tokens (or personal access through nx login) and reserve read-write for CI, so only trusted pipelines can populate the cache.
Option 2: a self-hosted cache
Organisations that cannot send build artefacts to a third party can host the cache themselves. Nx provides official cache packages for common storage backends and supports custom HTTP cache servers.
Object storage with an official package (S3 shown; GCS, Azure Blob and a shared file system are similar):
pnpm add -D @nx/s3-cache
pnpm nx g @nx/s3-cache:init
{
"s3": {
"region": "eu-west-1",
"bucket": "acme-nx-cache"
}
}
The configuration goes in nx.json. Keep secrets out of it: an optional encryption key for cached artefacts is supplied through an environment variable at run time rather than committed. CI provides credentials through the usual cloud environment variables or an OIDC role, and developers either get read access or use only their local cache.
Your own HTTP server. Nx can talk to any server that implements its self-hosted remote cache API — PUT and GET of task artefacts by hash — configured through environment variables:
export NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://nx-cache.internal.example.com
export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=***
pnpm nx run-many -t build
This is the route for teams that already run a Turborepo-style cache server or want full control; the server design questions are the same as in Self-Hosting a Turborepo Remote Cache.
Security: who may write to the cache
A remote cache is a trust boundary. Anyone who can write an entry can make every other machine replay arbitrary files as the output of a task — including build output that ends up deployed. Treat write access accordingly:
- Only CI on protected branches writes. Pull requests from forks and developer machines read only.
- Never let untrusted pull requests populate the cache used by
mainbuilds. Separate caches or read-only tokens for pull request pipelines prevent cache poisoning. - Encrypt at rest where the backend supports it, and restrict bucket access by role.
- Keep secrets out of task outputs. Anything written to
dist/or printed to the log is stored and replayed.
The same principles apply to Turborepo caches and are expanded in Remote Caching Setup.
CI configuration
jobs:
main:
runs-on: ubuntu-latest
permissions:
id-token: write # for OIDC access to the cache bucket
contents: read
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version-file: .nvmrc, cache: pnpm }
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/nx-cache-ci
aws-region: eu-west-1
- run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- run: pnpm nx affected -t lint test build
For pull requests from forks, skip the credential step so the job runs with the local cache only.
Cache entries across branches, platforms and Node.js versions
A shared cache is only safe if two machines produce the same hash exactly when they would produce the same output. Three dimensions deserve explicit thought.
Branches. Hashes do not include the branch name, so a task on a feature branch with the same inputs as on main shares its entry — which is what makes the cache useful. Isolation between trusted and untrusted runs therefore has to come from write permissions, not from hashing.
Operating systems and architectures. Tasks whose output depends on the platform — native builds, anything that embeds absolute paths, tools that emit platform-specific files — must include the platform in their inputs, typically with a runtime input such as { "runtime": "node -p process.platform + process.arch" }. Without it, a Linux CI run and a macOS developer can exchange incompatible outputs.
Node.js and tool versions. A task compiled with one TypeScript version and replayed for a project that has since upgraded is a stale hit if the compiler is not in the inputs. External dependency versions are hashed by default from the lockfile, but the Node.js version is not unless you add it; a sharedGlobals runtime input of node --version covers it for every task.
Rolling out a remote cache safely
Introduce the cache in read-only mode first. Let CI on main write while every other environment only reads, and compare build outputs from cache hits with fresh builds for a week — a scheduled job that runs with --skip-nx-cache and diffs dist/ is enough. Once the outputs match, widen reads to all pull request pipelines and developer machines. Only then consider allowing writes from pull request CI for same-repository branches, which raises hit rates further at the cost of trusting more pipelines. Rolling out in that order means a mistake in inputs is discovered while the cache can only replay results from the most trusted source.
Measuring the payoff
A cache that rarely hits costs storage and upload time for nothing. Nx prints a summary after each run — "Nx read the output from the cache instead of running the command for 31 out of 38 tasks" — which you can track over time. Watch three numbers: the remote hit rate for CI runs on pull requests, the time spent uploading on misses, and the size of the cache. A low hit rate usually means inputs are too broad or include non-deterministic files; high upload times mean outputs include more than they should, such as source maps or coverage you never replay.
Worked example: sharing results between CI and developers
A team adds an S3-backed cache with CI as the only writer and developers as readers. After merging a pull request, main's CI run populates the cache for every changed project. The next morning, developers pulling main run pnpm nx run-many -t build and get cache hits for everything built overnight — builds that took eight minutes locally now take thirty seconds. Hit rates in pull request CI climb from under 10% to around 75% once the team also fixes a production named input that included test snapshots.
Frequently Asked Questions
Can Nx and Turborepo share a remote cache? No. Their hashing and artefact formats differ. Each needs its own cache, even if both are served by the same infrastructure.
What happens if the remote cache is unavailable? Nx logs a warning and falls back to running tasks and using the local cache. Builds are slower but still succeed.
How long should cache entries be kept? Long enough to cover your typical branch lifetime and release cycle — commonly seven to thirty days. Lifecycle rules on the bucket enforce it automatically.
Should developers upload to the remote cache?
Usually not. Developer machines differ in ways that are hard to capture in inputs, and a developer-populated cache is harder to trust. Read-only access for developers captures most of the benefit, because CI builds main continuously.
Does remote caching help the first run on a new CI runner? Yes — that is where it helps most. A fresh runner has an empty local cache, so without a remote cache every task runs; with one, only tasks whose inputs changed since the last populated run execute.
Related
- Remote Caching Setup covers remote caching concepts across tools.
- Configuring Nx Named Inputs for Accurate Caching makes cache hits correct and frequent.
- Adding Nx to an Existing pnpm Workspace sets up the workspace this cache serves.
- Self-Hosting a Turborepo Remote Cache discusses cache server design choices that also apply here.