Nx remote cache
Nx remote cache for GitHub Actions, served by Magic Cache from your own S3 bucket. One action input, no Nx Cloud account, safe from cache poisoning.
Magic Cache serves Nx’s self-hosted remote cache ↗ from the RunsOn agent on every runner. Task outputs are stored in the cache bucket of your stack: there is no cache server to run, no Nx Cloud account or activation key, and nothing leaves your AWS account.
Availability#
Since v3.6.1, on runners with Magic Cache (extras=s3-cache), with Flex and Fleet, for Nx 20.8 and later. The Overview covers how build tool caches are scoped and stored.
Enable it#
jobs: build: runs-on: runs-on=${{ github.run_id }}/runner=8cpu-linux-x64/extras=s3-cache env: NX_NO_CLOUD: true # when nx.json has an nxCloudId steps: - uses: runs-on/action@v2 with: remote_cache: nx - uses: actions/checkout@v5 - uses: pnpm/action-setup@v4 - run: pnpm install --frozen-lockfile - run: pnpm exec nx run-many -t build testThe action points Nx at the agent with NX_SELF_HOSTED_REMOTE_CACHE_SERVER. Nx then prints [remote cache] for each task it restores.
- Safe from cache poisoning: a write never replaces an entry, and with the branch scopes, a pull request writes only to its own scope. It reads its base and default branch’s entries but can never put its outputs in front of them: the cache poisoning of CVE-2025-36852 ↗, for which Nx deprecated ↗ its S3, GCS, Azure and shared file system cache packages.
- Never fails a build: Nx stops a run when its cache answers an error. When S3 fails, the agent answers a miss or a skipped upload instead, and Nx runs the task.
- Per platform: Nx’s task hash leaves out the runner’s OS and architecture, so each platform has its own entries.
- Pull requests from forks read the cache: Nx skips the uploads the agent refuses.
- Container jobs reach the agent too.
On TanStack/query ↗ (nx run-many -t build test:types for 26 projects, 60 tasks, with type tests against six TypeScript versions) on an 8 vCPU runner:
| Build | Duration | Tasks restored |
|---|---|---|
| No cache | 87–88 s | none |
| No cache, uploading every task | 88 s | none |
| Same commit again, on a new runner | 2 s | all 60 |
On a new runner, after a one-line change in @tanstack/vue-query | 13 s | 55 |
On a new runner, after a one-line change in @tanstack/query-core, which every package uses | 82 s | 3 |
Uploading costs nothing measurable. A change rebuilds its package and every task that depends on it, so a change in query-core rebuilds almost everything.
A hit restores only the files a task lists in outputs, so list every file later steps need.
Nx uses the first cache it finds: Nx Cloud, when nx.json has an nxCloudId or nxCloudAccessToken, or the job sets NX_CLOUD_ACCESS_TOKEN, then a cache package such as @nx/s3-cache, then the self-hosted cache. Set NX_NO_CLOUD: true in the job’s env: to turn Nx Cloud off, and remove the cache package. A job that already sets NX_SELF_HOSTED_REMOTE_CACHE_SERVER keeps it: the action warns and leaves Nx alone.