← All posts
7 min readThe Cachely team

The easiest remote cache to add to an existing Nx workspace

If Nx already works for your team, the question is not which remote cache is most powerful - it is which one asks the least of a workspace that is already correct. That is a measurable thing: how many files change, how many new concepts the team has to learn, what happens when you take it out again. Measured that way, the modern Nx answer is surprisingly small: two environment variables, no plugin, and no edits to task definitions or the project graph.

What "easy to integrate" actually means

Integration cost is easy to hand-wave and easy to check. Before comparing options, write down what each one does to the five things you already depend on:

  • Files changed in the repo. A cache that needs a plugin, a runner entry, and a per-project setting is three places to review, merge, and later remove.
  • Task hashes. If adopting the cache changes how tasks are hashed, every existing local cache entry is dead on arrival and your first week looks slower, not faster.
  • CI configuration. Count the pipelines and job templates that need the new values, not just the first one you edit.
  • Accounts and onboarding. Per-seat provisioning is not a config step, but it is the step that stalls for two weeks while someone finds the budget owner.
  • Exit cost. The honest measure of a low-risk change is how quickly you can undo it.

First, ignore the tasksRunnerOptions advice

Plenty of guides still tell you to add a tasksRunnerOptions block to nx.json and point a custom task runner at a cache. That was the extension point of an earlier era, and it is not how remote caching is wired today. Since Nx 19.8 the CLI speaks a self-hosted remote cache API directly - an OpenAPI contract with two essential endpoints, GET /v1/cache/:hash and PUT /v1/cache/:hash, authenticated with a bearer token.

There is no runner to install and no hasher to override, which is the whole reason this path is cheap: Nx keeps computing hashes exactly as it does today, and the only new behaviour is where it looks for a hit. If a setup guide asks you to replace the task runner, it is describing a pre-19.8 workspace.

The three paths, ranked by what they touch

Nx Cloud

The first-party platform, connected with nx connect. It writes an identifier into nx.json, and in exchange you get remote caching plus the rest of the platform - distributed task execution, analytics, flaky-test detection. If you want those, this is the right answer and the integration cost is fair. If you only want shared artifacts, you are provisioning an account and a seat-based plan for one feature, and the honest comparison is about scope and pricing model rather than setup difficulty.

A bucket of your own

Historically the "just point it at S3" route, via @nx/s3-cache or a community plugin. Those first-party Powerpack cache packages are now deprecated, and the reason matters: a credential that can read and write every key cannot be made safe, which is the CREEP cache-poisoning class (CVE-2025-36852). The plugin install is the easy part; the retention policy, the token story, and the uptime are the part that keeps costing.

A managed implementation of the self-hosted API

The NX_SELF_HOSTED_REMOTE_CACHE_SERVER variable does not care who runs the server. Because the API is an open contract, a managed service can implement it and the client side stays two variables - which is what Cachely is:

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

One caveat worth more than the rest of this post: that URL must be the bare host, with no path. Nx appends /v1/cache/<hash> itself, so adding it yourself doubles the path and every request 404s. It is the single most common setup mistake.

What changes in your workspace, and what does not

Nx only consults the cache for targets you have declared cacheable, so the one thing worth checking in nx.json is that the targets you care about are:

{
  "targetDefaults": {
    "build": { "cache": true },
    "test": { "cache": true },
    "lint": { "cache": true }
  }
}

Most workspaces already have this, because it is what makes the local cache work. Nothing else moves. Project configuration, executors, custom plugins, nx affected, and the project graph are all untouched, because the remote layer sits below the executor interface. The commands your team types do not change; they just finish sooner.

Verifying it in one command

Run a cacheable task twice. The first run executes and uploads; the second prints a cache hit and completes in milliseconds. If you want to be certain the second hit came from the network rather than .nx/cache, clear the local cache with nx reset in between - or have a colleague run it.

Where teams actually get stuck

Almost none of the failures are conceptual. In practice they are four:

  • The appended path. Covered above, and worth repeating because a 404 on every request looks like an outage rather than a typo.
  • Nx version drift. Task hashes differ across Nx versions, so a team on mixed versions never shares entries. Every request succeeds and every one of them misses, which is the most confusing possible failure mode.
  • The job template nobody updated. The variables have to be present in every context that runs tasks. Miss one workflow file and that pipeline silently falls back to local-only behaviour, producing hit rates that look random.
  • Targets that were never cacheable. A target with no declared outputs caches its terminal output but restores no files, which reads as "the cache does nothing". If hit rates stay low for subtler reasons, over-keyed inputs are the usual culprit.

The fallback behaviour is worth stating plainly, because it cuts both ways: if the remote endpoint is unreachable mid-run, Nx keeps going against the local cache rather than failing the task. Good for availability, bad for diagnosis - which is why a hit-rate number you can actually look at matters more than it sounds.

The one setting to get right on day one

Give untrusted builds a read-only token. A pull request from a fork can modify the CI workflow without changing any file in the Nx task hash, which means it can hash to the same key a trusted build will later use and upload whatever it likes. Write access is therefore a supply-chain surface, and the boundary has to be enforced by the server - a build-tool flag that asks politely not to upload is guidance, not a control.

With Cachely that is a token scope: mint a read-write token for trusted CI and local development, a read-only one for pull requests, and rely on content-addressed artifacts being immutable so an existing key cannot be overwritten at all. See workspaces vs. tokens for how to structure that across repos, and the trust model for what is enforced where.

Rolling it back

Unset the two variables. Local caching behaves exactly as it did before, no data migration or cleanup is needed on the workspace side, and the only thing you lose is access to artifacts other machines produced. Teams do this deliberately while debugging a suspected cache issue, and re-enable it minutes later by restoring the variables.

That reversibility is the real argument for starting here. Compare it to the alternative that sometimes gets proposed in the same meeting - migrating the monorepo to a different build tool - which means rewriting task pipelines, redefining project boundaries, retraining the team, and abandoning the investment already made in Nx plugins and executors. One of those is an afternoon; the other is a quarter.

Once it is connected, read the numbers

The useful thing about a cache with a dashboard is that low hit rates stop being a vibe. Per-target and per-project breakdowns tell you which tasks still run every time, and that almost always points back at declared inputs rather than at the cache - a target keyed on something that changes on every commit will never hit, no matter where the artifacts live. Time saved and operations counts are also how you justify the line item; we wrote up what that arithmetic usually looks like.

And if your repo is not only Nx: a Cachely workspace is the cache boundary, not the tool boundary. Artifacts are namespaced per protocol inside it, so an Nx workspace and a Turborepo, Gradle, or Bazel build in the same repository share one workspace and one token rather than needing separate cache services.

Two variables, then measure

Point your existing Nx workspace at Cachely, run one task twice, and see the hit rate. Start free - no credit card.

Read the full guideNx remote cacheSet up the official Nx self-hosted cache and see how the task hash works.

Related posts

Put a shared build cache behind your builds
Free tier, no credit card - connect Nx, Turborepo, Gradle, or Bazel in about five minutes.
Start freeSee pricing