The complete guide

The Nx remote cache: setup, options, and how it actually works

Nx ships one of the best build caches in the JavaScript ecosystem, and its remote layer means a task built once - on any laptop or CI runner - is a cache hit everywhere. This guide explains how the Nx cache computes its task hash, how to set up remote caching with the official self-hosted API, what your options are (Nx Cloud, DIY, or a managed cache), and how to keep the whole thing secure and fast.

What is the Nx remote cache?

Every cacheable Nx task - build, test, lint, typecheck, anything with "cache": true - goes through the same decision before it runs. Nx computes a hash of everything that could affect the task's result and checks its local cache at .nx/cache. If the hash is there, Nx restores the declared outputs and replays the recorded terminal output; the task "runs" in milliseconds. If not, the task executes and its result is stored under that hash for next time.

The remote cache is a second layer behind the local one. On a local miss, Nx asks a shared cache server for the same hash; on a remote hit it downloads and replays the artifact, and on a full miss it runs the task and uploads the result so every other machine gets the hit. The lookup order is always local first, then remote - a warm local cache never touches the network. The result: work is done once per input state, across the whole team, not once per machine.

Remote caching composes multiplicatively with nx affected. Affected narrows the task graph to projects reachable from the files a diff touched; the cache then eliminates the tasks inside that narrowed graph whose inputs did not actually change. A typical pull-request pipeline running nx affected -t build test lint against a warm remote cache truly executes only the handful of tasks the diff invalidated - everything else is either outside the affected graph or a cache hit. That combination, not either mechanism alone, is what cuts CI wall time by the 30-70% teams report. (If you want the tool-neutral background on remote caching first, start with What is a remote cache?)

How Nx computes the task hash

The cache is only as correct - and only as fast - as its key. Nx builds each task's hash from a well-defined set of inputs, and understanding them is the difference between a 90% hit rate and a cache that misses on every commit:

  • Source files. The contents of the project's own files, filtered by the task's inputs. The production fileset convention excludes test files, so changing a spec does not invalidate the builds of every dependent project.
  • Dependency inputs. The ^production (dependencies' production files) entry pulls upstream projects into the hash, so a change in a shared library correctly invalidates everything downstream - and nothing else.
  • Named inputs. namedInputs in nx.json define reusable filesets like default and production once, so every target agrees on what "my sources" means.
  • Runtime and environment inputs. { "runtime": "node --version" } puts the toolchain version in the key; { "env": "MY_FLAG" } includes an environment variable that changes the output. Anything that affects the result but is not a file must be declared this way, or the cache will serve results built under different conditions.
  • External dependencies and global config. The lockfile and workspace-level configuration participate, so a dependency bump or an nx.json change re-keys affected tasks.

outputs are the other half of the contract: they tell Nx which files to store on a miss and restore on a hit ({workspaceRoot}/dist/{projectName}, coverage directories, and so on). Undeclared outputs are silently not cached, which shows up as "the build was a hit but my artifact is missing". Getting inputs and outputs right is the highest-leverage tuning work in an Nx workspace - the full walkthrough is in Tuning Nx cache inputs and outputs.

Setup: the self-hosted API and NX_SELF_HOSTED_REMOTE_CACHE_SERVER

Since Nx 19.8, remote caching has an official, open integration point: the self-hosted remote cache API. It is a small HTTP protocol - GET and PUT against /v1/cache/<hash> with a Bearer token - documented by Nx with an OpenAPI spec, and any server that implements it works with an unmodified Nx CLI. No plugin to install, no task runner to swap: Nx activates the remote layer when two environment variables are present - NX_SELF_HOSTED_REMOTE_CACHE_SERVER, the bare host of the cache server, and NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN, the credential it authenticates with:

NX_SELF_HOSTED_REMOTE_CACHE_SERVER=https://remote.cachely.dev
NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN=<your token>

Give NX_SELF_HOSTED_REMOTE_CACHE_SERVER the host only. Nx appends the /v1/cache/<hash> route itself, so writing the path in yourself turns every lookup into a 404 - the single most common setup mistake. With neither variable set, Nx falls back to the local cache in .nx/cache and nothing leaves the machine.

That is the entire client-side setup, and it works identically everywhere Nx runs. On a laptop, put the variables in your shell profile or a local .env file (Nx loads .env / .env.local automatically). In CI, set them as pipeline secrets - for GitHub Actions, an env: block at the workflow level covers every step:

# .github/workflows/ci.yml
env:
  NX_SELF_HOSTED_REMOTE_CACHE_SERVER: ${{ vars.NX_CACHE_SERVER }}
  NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN: ${{ secrets.NX_CACHE_TOKEN }}

From then on, every nx build, nx run-many, and nx affected consults the remote cache automatically. Behavior on failure is deliberately boring: an unreachable server or a 404 is treated as a miss, the task runs locally, and the result is uploaded when the server is reachable again. The cache is an optimization, never a dependency in the critical path. The step-by-step version - create a workspace, mint a token, set the variables - is in the docs.

Your options: Nx Cloud, DIY, or managed

The client protocol is settled; the real decision is who runs the server. There are three honest answers:

Nx Cloud

Nx Cloud is the Nx team's own platform, and remote caching is one feature of a much larger product: distributed task execution (spreading one command's task graph across many agents), managed CI agents, flaky-test detection and re-runs, and run analytics. If you want that platform - especially DTE, which nothing else replicates - Nx Cloud is the right choice, and its cache is first-party and excellent. The trade-off is that you adopt platform pricing (per-seat plans and credit-metered usage) to get caching, and for many teams caching is the only piece they came for. The detailed comparison is in Cachely vs Nx Cloud.

DIY self-hosted

The self-hosted API means you can run your own server: open-source implementations like nx-cache-server, or an object-storage bucket (S3, GCS, Azure) behind an adapter. You get full control and data locality, and you own hosting, scaling, auth, monitoring, and upkeep. One cautionary tale is worth knowing: the previous generation of first-party bucket adapters - the Nx Powerpack packages @nx/s3-cache, @nx/gcs-cache, @nx/azure-cache, and @nx/shared-fs-cache - were deprecated after CVE-2025-36852 showed their shared-bucket-credential design allowed cache poisoning that could not be patched out (more below). If a bucket is specifically what you were looking for, the options are laid out in the Nx S3 cache guide. Any DIY setup has to solve the same problem those packages could not: server-side write control. The full picture of what running your own cache involves is in Cachely vs a self-hosted Nx cache and the self-hosted-without-infrastructure walkthrough.

A managed implementation of the protocol

The third option keeps the self-hosted API's open protocol but hands the server to a service: someone else operates the storage, tokens, immutability enforcement, and monitoring, and your Nx config is still just the two environment variables - no lock-in beyond changing a URL. Cachely is in this category: a managed implementation of the Nx self-hosted remote cache API (and the Turborepo protocol) with read-only tokens and immutable artifacts enforced at the API, hit-rate and time-saved reporting, and flat pricing with no per-seat fees. It is the right fit when caching is what you want and operating cache infrastructure is not; it is the wrong fit if you need distributed task execution (use Nx Cloud) or on-premises data (self-host). See also Cachely vs the Powerpack cache packages for the migration path Nx points deprecated-adapter users at.

Security: CVE-2025-36852 and read-only tokens

A remote cache is a supply-chain component: trusted builds execute whatever it serves. The attack that matters for Nx is CVE-2025-36852, published as CREEP (Cache Race-condition Exploit Enables Poisoning). The shape of it: in a setup where untrusted builds share write credentials with trusted builds - the default when every pipeline holds the same bucket keys - a fork pull request can compute the hash a future trusted build will look up (hashes are deterministic; that is the whole point) and upload a malicious artifact under it first. When main later runs, it gets a "hit", restores the attacker's outputs, and ships them. No malicious code ever has to land in your repository.

Two server-side properties close the attack, and you should demand them from any Nx remote cache setup:

  • Read-only tokens for untrusted builds. PR and fork pipelines get tokens that can download (so they stay fast) but never upload. This must be enforced by the server per token - a bucket credential that can write anywhere fails this test by construction, which is exactly why the Powerpack adapters could not be fixed.
  • Immutable, content-addressed artifacts. Once a hash exists, re-uploads to it are rejected (409), so a known-good artifact can never be silently replaced - even by a compromised write token.

The full attack walkthrough is in Cache poisoning and CVE-2025-36852, and the trust model Cachely enforces - including per-workspace isolation and no repository access, with only customer-configured task outputs sent through the cache protocol - is documented on the security page.

Clearing, skipping, and disabling the Nx cache

Three different things get called "clearing the Nx cache", and mixing them up is why a cache appears not to reset. There is no nx cache clean command; the command is nx reset, and by itself it does more than empty the cache.

Clearing the local cache

npx nx reset                         # clear the local cache, workspace data, and stop the daemon
npx nx reset --only-cache            # clear only the local cache directory
npx nx reset --only-daemon           # only restart the daemon
npx nx reset --only-workspace-data   # only clear cached workspace metadata

The local cache lives in .nx/cache inside the workspace, so deleting that directory is equivalent to nx reset --only-cache. The important caveat: none of these touch the remote cache. A reset only empties the machine you ran it on, and the next task will happily restore the same outputs from the shared cache - which is usually what you want, and occasionally very confusing. If you genuinely need a remote entry gone, evict it on the server side; with Cachely that is a workspace-level operation in the dashboard, not a CLI flag.

The step-by-step version of this section - every nx reset flag, moving the cache directory, and the recurring problems that clearing does not fix - is in How to clear the Nx cache.

Skipping the cache for one run

To force real execution without changing any configuration, skip the cache at the call site:

# rerun tasks even when a cached result exists (local or remote)
npx nx run-many -t build --skip-nx-cache

# turn the remote cache off for this run; the local cache still works
npx nx run-many -t build --skip-remote-cache

--disable-nx-cache and --disable-remote-cache are accepted aliases of the same two flags. Mind the scope of each: --skip-nx-cache bypasses the cache entirely and does not write the fresh result back, while --skip-remote-cache disables remote reads and writes but leaves the local cache working normally. Neither can replace a bad remote entry - Cachely artifacts are immutable, so a poisoned or stale entry is removed by server-side eviction, not overwritten.

Disabling caching for a target

Skipping is per run; disabling is permanent and belongs in configuration. Set cache: false on the target - in targetDefaults for every project, or in a single project.json to scope it:

// nx.json - disable caching for one target everywhere
{
  "targetDefaults": {
    "deploy": { "cache": false }
  }
}

This is the right tool for targets that are not honestly cacheable: anything with side effects (deploys, publishes, database migrations), anything whose real output is not a file, and anything non-deterministic enough that replaying a previous result would be wrong. A task marked cache: true is a promise that the same inputs always justify the same outputs - if that is not true, disable caching rather than fight the hashes.

Troubleshooting low hit rates

A correctly wired remote cache with a low hit rate almost always has the same root cause: over-keying - something volatile is leaking into the task hash, so identical work produces different keys. The usual suspects:

  • Inputs that are too broad. If a target's inputs include the default "all project files" fileset, editing a README or a test invalidates production builds. Use the production named input for build targets and keep docs, specs, and config-that-does-not-matter out of it.
  • Environment leaks. A declared env input that changes every run - a build number, a timestamp variable, a per-runner path - re-keys every task. Declare only variables that genuinely change the output, and make CI set them deterministically.
  • Non-deterministic outputs upstream. A code generator that stamps dates, an install step that rewrites a file with unstable ordering, or a tool that embeds absolute paths makes a dependency's output differ per machine - and since downstream hashes include ^production, every consumer misses too. Fix the determinism at the source.
  • Divergent toolchains. If laptops run one Node version and CI another, a runtime input splits the cache in two. Pin versions (.nvmrc) so everyone hashes alike.

Diagnose with evidence, not guesses: run the same task twice from the same commit on two machines and compare hashes (nx run <project>:<target> --verbose prints hash details), and watch your cache server's per-task hit rates to see which targets never hit. A cache nobody measures quietly decays as inputs drift - the measurement side is covered in How much can a managed remote cache reduce CI build times?

Set it upSet up Nx with CachelyTwo environment variables, the bare-host rule, and the Nx versions that share a cache.

Related guides

Try the Nx remote cache on your monorepo
Free for developers. Two environment variables and your next nx affected run starts sharing cache.
Start freeSee pricing
FAQ

Nx remote cache: frequently asked questions

What is NX_SELF_HOSTED_REMOTE_CACHE_SERVER?
It is the environment variable the Nx CLI reads to find a remote cache server. Set it to the bare host of a server implementing the self-hosted remote cache API - for Cachely, https://remote.cachely.dev - with no trailing path: Nx appends the /v1/cache/<hash> route itself, so adding it yourself produces 404s on every lookup. Nx activates the remote layer only when this variable and NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN are both present; with neither set, Nx caches locally in .nx/cache and nothing leaves the machine.
What is NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN?
It is the credential Nx sends with every remote cache read and write, and it is what the server uses to decide which workspace the build is talking to and whether it may upload. Treat it as a secret: keep it in a CI secret store or an untracked .env file, never in nx.json or a committed workflow. Give untrusted builds - pull requests and forks - a read-only token so they stay fast but cannot write an artifact a trusted build would later restore.
What is the Nx remote cache?
A shared layer behind the local Nx cache (.nx/cache). Nx hashes every cacheable task from its inputs; on a local miss it asks a shared cache server for that hash, downloads and replays the stored outputs on a hit, and uploads the fresh result on a miss. A task built once - on any laptop or CI runner - becomes a cache hit for every machine on the team.
Do I need Nx Cloud to use remote caching?
No. Since Nx 19.8 the CLI ships an official self-hosted remote cache API: an open HTTP protocol activated by two environment variables (NX_SELF_HOSTED_REMOTE_CACHE_SERVER NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN). Any conforming server works - self-hosted or managed. Nx Cloud is a broader platform (distributed task execution, CI agents, analytics) and is the right choice when you want those features, but remote caching alone does not require it.
Is the self-hosted remote cache API free?
The API itself is a protocol, not a product - it is documented by Nx with an OpenAPI spec and free to implement or use with an unmodified Nx CLI. What you pay for is whatever backend you choose to run or subscribe to: your own server and storage if you self-host, or a managed service if someone else operates it.
How do read-only cache tokens work with Nx?
The token in NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN identifies the build to the cache server, which enforces permissions per token. A read-only token lets a pipeline download artifacts (so it stays fast) but rejects uploads. Give read-only tokens to untrusted builds - pull requests and forks - so they can never plant a poisoned artifact that a trusted build would later restore (the CVE-2025-36852 attack).
Does the remote cache work with nx affected?
Yes, and the two compose: nx affected narrows the task graph to projects reachable from the changed files, then the cache eliminates the tasks inside that graph whose inputs did not actually change. A typical PR pipeline against a warm remote cache only truly executes the handful of tasks the diff invalidated.
What happens on a cache miss or if the server is unreachable?
Nx just runs the task locally, exactly as it would without a cache, and uploads the result when it can. A missing entry, an outage, or an over-quota workspace all degrade to a normal build - the remote cache is an optimization, never a dependency in the critical path.
Is my source code uploaded to the remote cache?
Nx hashes inputs locally and sends configured task outputs, such as compiled files, coverage reports, and terminal output. Cachely never clones or connects to your repository, but any source or source-derived file included in your declared outputs becomes part of the cache artifact.
Why is my Nx cache hit rate low?
Almost always over-keying: something volatile is leaking into the task hash. The usual causes are inputs that are too broad (test files invalidating production builds - use the production named input), environment variables that change every run, non-deterministic outputs upstream (timestamps, absolute paths), or divergent toolchain versions between laptops and CI. Compare hashes for the same commit on two machines to find the leak.
How do I clear the Nx cache?
There is no nx cache clean command. Run nx reset to clear cached state, or nx reset --only-cache to empty only the local cache directory (.nx/cache) while leaving the daemon and workspace data alone. Related scopes are --only-daemon and --only-workspace-data. Neither form deletes anything from the remote cache, so the next run can still restore the same outputs over the network.
How do I disable or skip the Nx cache for one run?
Pass --skip-nx-cache to bypass the cache entirely for that invocation (nothing is read or written back), or --skip-remote-cache to turn off remote reads and writes while the local cache keeps working. They accept --disable-nx-cache and --disable-remote-cache as aliases. To disable caching permanently for a target, set cache: false for it under targetDefaults in nx.json - the right choice for targets that are not a deterministic function of their inputs, such as deploys or anything that talks to a live service.