What Nx does without the cloud

Nx Core is a task runner, and most of what makes it fast happens on a single machine. Nx Cloud is easier to evaluate once that baseline is clear.

The project graph and the task graph

Nx reads your workspace configuration and source imports to build a project graph, which records which projects depend on which. When you run a target such as build or test, Nx turns that into a task graph. Each node is a concrete task, for example "build api", and each edge is an ordering constraint, for example "build shared-utils first." This graph drives everything else.

Ordering and parallelism

Since Nx knows the dependencies, it runs tasks in a valid order and runs independent tasks in parallel, up to a configurable limit. You don't maintain a hand-written pipeline that falls out of date whenever someone adds a project.

The local cache

Before running a cacheable task, Nx computes a hash of its inputs. If that hash already exists in the local cache, Nx restores the stored output and terminal logs rather than running the task again.

Affected projects

nx affected compares your changes against a base Git ref. It runs tasks only for the projects you touched and the projects that depend on them.

Where Nx Cloud fits

Each of these mechanisms stops at the edge of one machine. The cache lives on one disk, and parallelism is bounded by one machine's cores. Nx Cloud extends the same hashing and the same task graph across machines. It adds a shared remote cache and distributes tasks across CI agents. It does not replace any of these mechanisms.

How task caching works and why inputs decide everything

Before running a task, Nx computes a hash from everything the task declares it depends on: the project's source files, the outputs of the projects it depends on, the target's configuration and options, and any environment variables or runtime values you list. If that hash matches a stored entry, Nx skips the work. It restores the recorded output files and replays the terminal logs, so the result looks the same as a fresh run.

The hash only covers what you declare. In the example that follows, nx.json defines a production named input that includes the project's files but excludes spec files. That way, editing a test does not invalidate the build. The build target consumes production and ^production, which pulls in the production inputs of its dependencies. It adds an env input for API_URL and a runtime input that runs node --version, so a Node upgrade also changes the hash. outputs points at dist/{projectRoot} so Nx knows which files to store and restore. cache: true marks the target as cacheable.

Where this breaks

If the hash is missing an input, you get a false cache hit. Suppose a build reads API_URL but API_URL is not declared as an input. Nx will then restore an artifact built against the wrong endpoint.

The second failure mode is caching tasks that are not pure functions of their inputs. Never cache deploys, migrations, tasks that call live services, or tests that depend on the clock. A cache hit on any of these means the work silently did not happen.

// nx.json (Nx parses this file as JSONC, so comments are allowed)
{
  "namedInputs": {
    // Every file in the project, plus workspace-wide files listed in sharedGlobals
    "default": ["{projectRoot}/**/*", "sharedGlobals"],
    "sharedGlobals": ["{workspaceRoot}/tsconfig.base.json"],
    // Production inputs exclude specs, so editing a test does not invalidate the build
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  },
  "targetDefaults": {
    "build": {
      "cache": true,
      "dependsOn": ["^build"],
      "inputs": [
        "production",
        // The production inputs of every project this one depends on
        "^production",
        // The build reads API_URL, so it must be part of the hash
        { "env": "API_URL" },
        // A Node upgrade changes the hash
        { "runtime": "node --version" }
      ],
      // Files Nx stores after a run and restores on a cache hit
      "outputs": ["{workspaceRoot}/dist/{projectRoot}"]
    },
    "test": {
      "cache": true,
      "inputs": ["default", "^production"]
    },
    // Side effects or live dependencies: a cache hit would mean the work never ran
    "deploy": { "cache": false },
    "db-migrate": { "cache": false },
    "e2e-against-staging": { "cache": false }
  }
}

An nx.json that declares named inputs, an env and runtime input, outputs and cacheability for build, and explicitly disables caching for deploy, migration and live-service targets.

What a remote cache adds, and how it differs from CI dependency caches

A remote cache is the local cache with a shared backend. Each task's hash, computed from its inputs, becomes a key, and the stored outputs and terminal logs become the value. Any developer laptop or CI runner that computes the same hash can download the result instead of running the task again.

In CI this shows up in three ways. A re-run after a flaky failure skips every task that already succeeded. A pull request rebased onto a main that CI has already built gets hits for every project the PR didn't touch. Parallel jobs stop rebuilding the same shared library, because the first job to finish uploads the result and the others reuse it.

CI dependency cache Nx remote cache
Unit Directory snapshot (node_modules, build folders) Outputs of a single task
Key Lockfile hash, branch name Hash of the task's declared inputs
Partial reuse All or nothing Per task
Correctness depends on Key choice being conservative enough Inputs being declared completely

The two approaches complement each other. Keep caching installed dependencies, and use Nx's cache for task results.

The trade-off is reach. A missing input used to produce one bad local hit. Now it produces a bad artifact on every machine. For this reason, many teams let only CI write to the cache and give laptops read-only access, which limits the risk of cache poisoning from untrusted or misconfigured environments. Expect cold runs when shared libraries, the lockfile, or global configuration change, because those inputs feed almost every hash.

How distributed task execution works

Caching avoids repeated work. Distribution spreads the remaining work across machines. Instead of each CI job running a fixed slice of commands, the main job hands Nx the targets it needs and Nx Cloud assigns individual tasks from the task graph to a pool of agent machines. A task is assigned only after its dependencies have finished, so a library's build always completes before the apps that consume it are built. The outputs of each finished task are uploaded and then restored on whichever agent picks up the downstream task. That agent may never have built the library itself.

            ┌──────────── main CI job ────────────┐
            │ computes task graph, waits, reports │
            └───────┬──────────┬──────────┬───────┘
                    ▼          ▼          ▼
               [agent 1]   [agent 2]   [agent 3]
              lib:build    api:lint    e2e:file-a
                  │            ▲           ▲
                  ▼            │           │
            ┌─────────── remote cache / artifacts ─────────┐
            │  lib outputs ──► restored for app:build      │
            └──────────────────────────────────────────────┘

Static sharding uses lists like "job 3 runs projects M–Z," which someone has to keep in balance by hand. Every new project or slow test file throws that balance off. Assigning tasks dynamically from the graph removes that upkeep.

A single slow target, such as a large e2e suite, still limits how much distribution helps. Splitting it into one task per spec file gives the scheduler smaller units to spread across agents.

Nx Cloud can also flag a task as flaky when the same input hash has produced both a pass and a failure, and it retries such tasks automatically. A retry gives you a green run, but it does not make the result trustworthy. Only results that are deterministic for a given set of inputs should be cached, because a cached failure or a cached accidental pass gets replayed to everyone who shares that hash.

┌─────────────────────────── main CI job ────────────────────────────┐
│ nx affected -t build lint e2e  →  task graph to Nx Cloud           │
│ waits for every task, then reports one combined result             │
└──────────────────────────────┬─────────────────────────────────────┘
                               ▼
┌────────────────────── Nx Cloud orchestrator ───────────────────────┐
│ assigns a task only after its dependencies have finished           │
│ slow e2e target split into one task per spec file                  │
│ same hash passed and failed → flagged flaky, retried               │
└───────┬──────────────────┬──────────────────┬──────────────────────┘
        ▼                  ▼                  ▼
  ┌───────────┐      ┌───────────┐      ┌───────────┐
  │ agent 1   │      │ agent 2   │      │ agent 3   │
  │ lib:build │      │ api:lint  │      │ e2e:spec-a│
  │           │      │ app:build │      │ e2e:spec-b│
  └─────┬─────┘      └─────▲─────┘      └─────┬─────┘
        │ upload lib       │ restore lib       │ upload results
        ▼ outputs          │ outputs           ▼
┌────────────────────────────────────────────────────────────────────┐
│ remote cache / artifacts, keyed by task input hash                 │
│ lib:build outputs restored on the agent that runs app:build        │
│ only deterministic results are trusted and cached                  │
└────────────────────────────────────────────────────────────────────┘

The main CI job sends the task graph to Nx Cloud, which hands tasks to agents in dependency order, while finished outputs pass through the remote cache so that downstream tasks like app:build can run on a different agent.

Doing the math: cache hit rate and the critical path

Illustrative estimate

None of these inputs are measured. They are assumptions picked to show how the numbers interact:

  • 400 tasks in the affected set
  • 90 seconds average per task
  • 80% cache-hit rate
  • 8 agents
  • a 22-minute longest dependency chain, made of tasks that all miss because the change touched a shared library at the bottom of the graph
  • 3 minutes of overhead for coordination and artifact transfer

Uncached work is 400 × 90 s = 600 minutes. At an 80% hit rate, 80 tasks still execute, which is 120 minutes of work. Eight agents split that into about 15 minutes each. The critical path is 22 minutes, though, and no agent can start a task before its dependencies finish. The chain sets the pace, and the agents wait on it.

A rough formula for your own repo:

wall-clock ≈ max(N × T × (1 − h) / A, C) + O

N is task count, T is average duration, h is hit rate, A is agent count, C is the critical path, and O is overhead. The formula treats cache hits as free. They aren't: restoring outputs takes time, so count large artifacts in O.

Scenario Work per agent Critical path Wall-clock
Baseline, 8 agents 15 min 22 min ~25 min
16 agents 7.5 min 22 min ~25 min
8 agents, long e2e task split 15 min 12 min ~18 min
8 agents, 60% hit rate 30 min 22 min ~33 min

Once C is larger than the per-agent work, more agents only add idle time and overhead. To get faster from there, shorten C. You can split a long test task into shards, or break a shared library into smaller projects so that fewer tasks sit downstream of it.

Who should use it, when to avoid it, and the alternatives

Nx Cloud pays off when several conditions hold together. The task graph is large, so most tasks in a typical PR are unaffected and can be restored from cache. CI runs many jobs in parallel, so a shared cache stops each job from rebuilding the same work. Pipelines get re-run often, whether from flaky tests or small fix-up commits. Tasks are also deterministic enough that a matching hash actually means a matching output.

Hold off in a few cases. In a small repo, local caching and affected already capture most of the savings. If the graph is dominated by one long serial chain, there is little work to spread across agents. With heavy non-determinism, such as timestamps in outputs or tests that depend on network state, you get cache misses or, worse, stale hits. Strict data-residency rules can also rule out sending build artifacts to an external service.

Cache lookups and agent coordination now depend on that service being up. Large artifacts like Docker layers or bundled apps cost network time to upload and download, and sometimes fetching takes longer than rebuilding. Before you rely on the service, confirm that CI falls back to running tasks locally when it is slow or unreachable, and work out how long a full uncached run takes so you know your worst case.

Option What you get Trade-off
Local cache only No setup, no external dependency Every CI machine starts from scratch
CI-provider dependency caching Faster installs Caches node_modules, not task outputs
Self-managed remote cache Data stays on your storage You own uptime, eviction and access control
Manual sharding Parallelism with no extra service Static splits drift out of balance as the repo changes

Adoption path and impact on cost and engineering time

Stage 1: make local caching correct

Make sure every cache hit is trustworthy before any artifact leaves a laptop. Run a target twice and confirm the second run replays from cache. Then change a file that should not matter and check that the hash stays the same. Use nx graph to see the project graph, nx show project <name> to see the inputs a target resolves to, and nx affected -t test to confirm only the expected projects run. While debugging, add --skip-nx-cache to force real execution, and compare its output with the cached version. Every time a stale artifact appears, record the task hash, the commit, and the input that was missing. That record shows which input definition to fix.

Stage 2: remote cache, CI writes only

Give CI read-write access and give developers read-only access. That way, a single misconfigured laptop cannot poison shared results. Measure the hit rate per target in CI for a few weeks before you change anything else.

Stage 3: distribute selectively

Add agents only when the critical path is short compared with the total task time. If one long chain dominates the run, more agents will not shorten it.

Illustrative estimate (assumed numbers, not a benchmark):

hours saved/month = PR runs × miss-path minutes avoided ÷ 60

With 800 runs and 12 minutes avoided per run: 800 × 12 ÷ 60 = 160 engineer-waiting hours.

Weigh that figure against the service fees, the agent compute, and the ongoing work of keeping input definitions accurate. Then add back the time no longer spent rebalancing static shard configurations.

#!/usr/bin/env bash
# Stage 1 check: confirm a cached build replays the same output as a real build.
# Placeholders: project "web-app", outputs in dist/apps/web-app. Adjust to your workspace.
set -euo pipefail
PROJECT=web-app
OUT=dist/apps/$PROJECT

# Show the inputs and outputs the build target resolves to.
nx show project "$PROJECT" --json > project-config.json

# Export the project graph so you can review dependencies offline.
nx graph --file=project-graph.json

# Confirm that only the projects you expect are affected relative to main.
nx affected -t test --base=origin/main --head=HEAD

# Force a real build that bypasses the cache, then checksum its output.
nx run "$PROJECT":build --skip-nx-cache
(cd "$OUT" && find . -type f -exec sha256sum {} + | sort -k2) > fresh.sha

# Run the build again so it replays from cache. Nx reports the cache hit in its output.
rm -rf "$OUT"
nx run "$PROJECT":build
(cd "$OUT" && find . -type f -exec sha256sum {} + | sort -k2) > cached.sha

# Matching checksums mean the cache hit is trustworthy. A mismatch points to an undeclared input.
if diff -u fresh.sha cached.sha; then
  echo "OK: cached output matches a fresh build"
else
  # Log the commit and target so you can trace which input definition needs fixing.
  echo "MISMATCH at $(git rev-parse --short HEAD) for $PROJECT:build" | tee -a cache-mismatch.log
  exit 1
fi

A Stage 1 script that inspects a target's resolved inputs and the project graph, then compares a cache-bypassing build against a cached replay and logs the commit whenever their outputs differ.

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 cloud, 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.