What Nx caching is and how computation caching works

Computation caching means treating a task as a pure function: given the same inputs, it produces the same outputs, so the result of one run can stand in for the next. Nx applies this idea to targets such as build, test and lint. It skips work you have already done rather than doing the same work faster.

What happens when Nx runs a task

Before executing a cacheable task, Nx computes a hash that summarises everything the result should depend on. By default this covers:

  • the source files of the project and of the projects it depends on
  • relevant configuration, such as the target definition and global config files
  • any environment variables or runtime values you declare as inputs

Nx then looks up that hash. On a miss, it runs the task and stores two things under the hash: the terminal output and the files written to the declared output locations, such as a dist directory. On a hit, it skips execution entirely. It replays the logged output and restores the files to disk, so downstream tasks and humans see what a real run would have produced (Nx docs).

Why inputs and outputs matter

Nx cannot detect what a task actually reads or writes. It trusts what you declare. If a task depends on an environment variable you never listed, two different builds can share one hash, and Nx will serve the wrong artifact. If you omit an output folder, a cache hit reports success but leaves that folder missing or stale. Treat inputs and outputs as a correctness contract, not a speed setting.

 +------------------+  +------------------+  +------------------+
 | Source files     |  | Configuration    |  | Declared env     |
 | (project + deps) |  | (target, global) |  | vars / runtime   |
 +--------+---------+  +--------+---------+  +--------+---------+
          |                     |                     |
          +---------------------+---------------------+
                                |
                                v
                    +-----------------------+
                    |   Compute task hash   |
                    +-----------+-----------+
                                |
                                v
                    +-----------------------+
                    | Look up hash in cache |
                    +-----+-----------+-----+
                          |           |
                     miss |           | hit
                          v           v
        +---------------------+   +-------------------------+
        | Run the task        |   | Skip execution          |
        +---------+-----------+   | Replay terminal output  |
                  |               | Restore output files    |
                  v               |   (e.g. dist/)          |
        +---------------------+   +-------------------------+
        | Store under hash:   |
        | terminal output +   |   The hash sees only what you declare:
        | declared outputs    |   inputs and outputs are a contract.
        +---------------------+

Nx hashes the declared inputs of a task, runs and stores the result on a cache miss, and on a hit replays the terminal output and restores the declared output files instead of running the task.

Who should use it and which tasks are safe to cache

Nx caching pays off most for backend and platform teams running monorepos where many services share libraries and CI rebuilds the same projects on every push. If a pull request touches one service, the other twenty in the workspace should not be rebuilt and retested. The more often identical work repeats across branches, retries, and developer machines, the more a cache saves.

The rule that decides what can be cached is determinism: the same inputs must always produce the same output. Nx's documentation states that cacheable operations must be side-effect free, and uses e2e tests against a backend API as an example of what cannot be cached, because the backend can change the result. 1 If a task's result depends on anything Nx does not hash, a cache hit can replay a result that is no longer true.

Task Cache it? Reason
build Yes Output comes from source, config, and dependencies
test (unit) Yes Isolated, with no external services
lint Yes Pure function of source and rule config
e2e against a live backend No The backend state affects the outcome
deploy, publish No The side effect is the point of the task
Anything reading the clock or network No The result changes between runs

Some tools look deterministic but are not. Common cases are build stamps that embed the current timestamp, bundlers or code generators that emit random IDs, and source maps or manifests that record absolute paths such as one developer's home directory. These builds are still cacheable, but you need to fix them first. Pin or remove the timestamps, seed or disable the random IDs, and make the paths relative. Until then, cached artifacts will differ from fresh ones in ways that are hard to trace.

Enabling caching with targetDefaults in nx.json

Once your tasks are deterministic, you can turn caching on. Nx handles this per target, and the simplest approach is to set it once in nx.json under targetDefaults. Add an entry keyed by the target name, for example build and test, and give each entry "cache": true. Every project in the workspace that defines a target with that name inherits the setting. You don't need to touch any project.json. The example below shows the shape, and the Nx documentation on caching task results uses the same pattern (source).

Why workspace-wide defaults

You could add "cache": true to each project's configuration instead. That works for five projects and becomes a problem at fifty. Per-project settings drift: someone copies an older project.json that never enabled caching, or disables it while debugging and forgets to revert. The result is a cache hit rate that varies by project for reasons nobody can explain.

With the setting in targetDefaults, there is one place to check which targets are cacheable. A change to that policy is a single reviewable diff. A project that needs different behaviour can still override the default in its own project.json, and that override then stands out as a deliberate exception. Leave targets with side effects, such as e2e suites that hit a live backend, out of the defaults entirely.

This is only the switch

"cache": true tells Nx it may cache a target. It does not tell Nx which files and environment values affect the result, or where the artifacts end up. If inputs are too narrow, Nx serves stale results. If outputs are missing, a cache hit replays the terminal log but leaves out the build directory. The next section covers how to declare both.

{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "cache": true
    },
    "test": {
      "cache": true
    }
  }
}

A minimal nx.json that enables caching for every project's build and test targets through targetDefaults. Targets with side effects, such as e2e, are left out, and inputs and outputs still need to be declared.

Declaring cache inputs and outputs accurately

Named inputs

Define reusable file sets once in namedInputs in nx.json, then reference them from targets. A common pattern is a default set covering {projectRoot}/**/* and a production set that starts from default and subtracts paths with !, such as !{projectRoot}/**/*.md and !{projectRoot}/**/*.spec.ts. Point build at production and test at default. A README edit or a new spec file then leaves the build hash unchanged, while tests still rerun when their files change.

Dependency-aware inputs

Prefixing a named input with ^, as in ^production, adds the matching inputs of every project the current project depends on. Without it, a change inside a shared library would not alter the hash of the application that bundles it, and Nx would replay an application build that predates the library change.

Outputs

Declare outputs as paths with tokens, for example {workspaceRoot}/dist/{projectName}, so one target default applies correctly to every project. If an output is missing or points at the wrong folder, Nx caches nothing for it. On a later hit, Nx reports success and prints the original log, but the build directory is never restored. The failure appears downstream, when a packaging or deploy step cannot find its files.

Environment and runtime values

File hashes do not capture configuration. Add { "env": "API_URL" } for each variable that affects the output, and { "runtime": "node --version" } for toolchain versions. Two different configurations then produce two different hashes and cannot share a cache entry.

Getting the balance right

Under-declared inputs produce false hits, which ship stale artifacts that look valid. Over-declared inputs, such as hashing the whole workspace or volatile variables like timestamps, produce constant misses and add hashing and storage cost without any reuse. Of the two, false hits are worse, because a constant miss costs time but a false hit can ship a wrong build without anyone noticing.

{
  // nx.json (Nx parses this file as JSON with comments)
  "namedInputs": {
    // Every file in the project
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    // Source that affects shipped output: no docs, no tests
    "production": [
      "default",
      "!{projectRoot}/**/*.md",
      "!{projectRoot}/**/*.spec.ts"
    ],
    // Workspace-wide values that change what any build produces
    "sharedGlobals": [
      { "env": "API_URL" },
      { "runtime": "node --version" }
    ]
  },
  "targetDefaults": {
    "build": {
      "cache": true,
      // ^production pulls in the production inputs of every dependency
      "inputs": ["production", "^production"],
      // Must match where the build actually writes, or a hit restores nothing
      "outputs": ["{workspaceRoot}/dist/{projectName}"]
    },
    "test": {
      "cache": true,
      // Tests rerun when spec files change
      "inputs": ["default", "^production"]
    }
  }
}

An nx.json that excludes docs and specs from build hashes, uses ^production to include dependency inputs, hashes API_URL and the Node version, and declares a tokenized build output path.

Verifying and debugging the cache

That risk is the reason to check the cache on purpose instead of assuming it works. Run this check once for each target you mark as cacheable, and again whenever you change its inputs or outputs.

A four-step audit

  1. Run the task twice. The first run should execute. The second should be reported as read from the cache. A hit alone doesn't prove much, so also delete the output directory before the second run and confirm that Nx puts the files back. If the restored artifacts differ from a fresh build, outputs is missing a path or the task isn't deterministic.
  2. Change something irrelevant. Edit a file you excluded from the inputs, such as a README, and run the task again. It should still hit. A miss here means your inputs are too broad, and you'll get the constant misses described above.
  3. Change something relevant. Edit a source file, a dependency's code or an environment variable the task reads. The next run must miss. A hit here is a false hit, and fixing it comes before any tuning for speed.
  4. Revert the change. Run once more. The original hash should match again and hit, which shows the hash depends only on the content of the inputs.

Clearing state while debugging

The local cache keeps growing as it stores results for every hash it has seen. Old entries can also hide problems after you edit nx.json: a run can still hit on a result stored before your edit and make a broken change look fine. Run nx reset to clear the local cache and Nx's stored workspace state, then repeat the audit from a clean start. The cost is short-lived. Until the cache fills again, every task runs in full, so expect the first runs after a reset to be slow.

#!/usr/bin/env bash
set -euo pipefail
PROJECT=my-app
OUT=dist/apps/my-app

npx nx reset

npx nx build "$PROJECT"
cp -r "$OUT" /tmp/fresh-build

rm -rf "$OUT"
npx nx build "$PROJECT"
diff -r /tmp/fresh-build "$OUT" && echo "restored output matches fresh build"

echo "" >> apps/my-app/README.md
npx nx build "$PROJECT"

echo "// audit" >> apps/my-app/src/main.ts
npx nx build "$PROJECT"

git checkout -- apps/my-app/README.md apps/my-app/src/main.ts
npx nx build "$PROJECT"

A repeatable shell audit for one Nx target: reset the cache, confirm a hit that restores the same output, then check that an irrelevant edit still hits, a relevant edit misses and reverting the edit hits again.

Local cache vs remote cache, and the alternatives

The local cache lives on the machine that ran the task. It speeds up a developer's second build or a re-run on the same CI agent, but a fresh, ephemeral CI runner starts with an empty cache and recomputes everything. A remote cache stores task results in shared storage, so a build computed in one CI run can be restored by the next run or by a developer's laptop, provided the computed hash matches. Nx documents a managed remote cache built on Nx Cloud, which you enable by running npx nx@latest connect in the workspace (source).

Sharing results also means sharing trust. Nx restores both the terminal output and the files a task produced. Anyone who can write to the cache can therefore place artifacts that other machines will use as if they had built them. Limit write access to trusted pipelines, such as builds on protected branches, and give forks and untrusted branches read-only access or none.

The network is a cost too. Every cache hit involves a lookup and a download. For tasks that finish in a few seconds, or tasks with very large outputs, recomputing can take about as long as fetching. A managed service also adds an external dependency, with its own accounts, credentials, and rules for where your build artifacts are stored.

Option Who benefits Main trade-off
Remote cache CI runs and developers Trust boundary, network, external service
Local cache only The machine that ran the task Cold CI runners recompute
No caching Nobody Every task runs in full
Affected-only selection CI Skips untouched projects, but rebuilds affected ones in full

Impact on CI cost and engineering time

Affected-only selection limits which projects run. Caching determines how much of that remaining work is actually repeated. The savings come mostly from how often a task's hash matches, not from how fast the task runs.

Illustrative estimate (assumed numbers, not a benchmark or measured result). Assume 40 cacheable tasks per pipeline, an average task duration of 3 minutes, and 30 pipeline runs per day. The hit rates and the 20% speed-up below are also assumed values chosen for comparison, not observations.

Compute minutes per day = tasks × avg duration × runs × (1 − hit rate)

Scenario (illustrative) Assumed avg duration Assumed hit rate Compute min/day (illustrative)
No cache 3.0 min 0% 3,600
Cache, modest hit rate 3.0 min 50% 1,800
Tasks optimized 20% faster 2.4 min 50% 1,440
Cache, tight inputs 3.0 min 80% 720

Illustrative estimate: every figure in this table is calculated from the assumed inputs above using the formula shown. None of these values were measured, and your own numbers will depend on your workspace and pipeline.

In this illustrative model, making every task 20% faster saves 360 minutes a day, while raising the assumed hit rate from 50% to 80% saves 1,080. Hit rate usually improves when inputs are narrower, for example when READMEs, unrelated config, and volatile environment variables are left out of the hash. The model ignores the time spent restoring cached artifacts, so real savings will be somewhat lower.

These minutes matter for engineers when cached tasks sit on the pipeline's critical path. In that case a pull request goes green sooner, so developers wait less for CI and switch context less often.

Caching also has costs:

  • Ongoing audits. Inputs and outputs must be reviewed whenever someone adds a code generator, changes a tool's config file, or introduces a new environment variable.
  • False hits. If an input is under-declared, Nx can restore a stale artifact and report success, and that artifact can ship. A scheduled pipeline that runs with caching disabled and compares results will catch this drift before a release does.

Budget for both of these costs when you estimate what caching will save.

How OneEnv can help

Disclosure: OneEnv is the product behind this blog.

OneEnv (oneenv.io) is a workspace-based governance platform for the things services share — environment variables, database structure, API routes and validation rules. A change to a shared value is staged on a branch, reviewed once, and then every affected service explicitly approves or rejects it before it goes live, so you can see who a change touches before it ships instead of finding out in production.

Applied to nx caching, this is where it helps:

Saves time

  • Every service that inherits a shared value is listed in one place, so nobody searches repos and chats to find out who is affected.
  • One review per branch replaces coordinating each team separately; each service still keeps its own approve/reject.
  • Reviewers see the exact diff and the list of affected services up front, so approval is a decision rather than an investigation.

Saves cost

  • Fewer production incidents and rollbacks caused by a shared value, route or column changing under a consumer unannounced.
  • Less duplicated configuration to maintain: one shared definition inherited by many services.
  • Per-service approve/reject means a bad shared value can be refused where it would break something, before it is live.
  • OneEnv has a free plan (1 workspace, unlimited services and members, full governance), so trying it costs nothing.

These are qualitative benefits, not measured results — how much you save depends on how many services share the value.

Want to try it? Start at oneenv.io.