We raised $3M for agent-native runners

Caching and setup

Reuse dependencies and build outputs where it saves time. Compare cache and setup changes, including the cost of restoring and saving them.

Where to start

Diagnose first: does cache restore plus save cost less than the download or build it avoids, across both cache hits and misses?

A useful cache saves more work than it adds. Compare restore, install, build, and save timings across equivalent runs. Use the examples below to choose what to cache, when to refresh it, and which setup work to share.

19 techniques with source-backed company examples.

CI technique

Cache package downloads and dependency stores

Restore package-manager downloads or dependency stores while keeping a locked install that validates the requested dependency graph.

Cache Go modules and build output

The first CI workflow for the project pins Go from go.mod and sets the setup action's cache input explicitly (the action caches by default in the version it uses), which restores downloaded modules and build output on later runs. Tests run with -count=1, so Go executes them every time.

Use it when
Go jobs download modules or rebuild unchanged packages on every run.
Before applying it
Key the cache on go.sum and the toolchain version, and measure restore time against a cold run on your runner.

Cache the pnpm store, in the right setup order

Linear’s SDK repository installs pnpm before setup-node and caches the pnpm store in its build, release, schema and dependency workflows, keyed on pnpm-lock.yaml.

Use it when
pnpm installs repeatedly download the same dependency content.
Before applying it
Keep the install and every check running on a cache hit; the cache shortens the download.

Cache Yarn downloads while keeping a locked install

Linear’s Zapier integration enables setup-node’s Yarn cache and installs with yarn install --frozen-lockfile, in its test job and its new release workflow.

Use it when
Yarn installs repeatedly fetch the same dependency content.
Before applying it
Point the cache at the actual lockfile, keep frozen installs, and keep untrusted runs from writing caches that privileged jobs restore.

Cache the native dependencies behind a JavaScript build

The React Native jobs already restored pnpm and Bundler caches, while the Android jobs still downloaded Gradle distributions during each run. Checkout Kit turned on the setup-java Gradle cache, keyed on the Android, React Native and sample-app Gradle and package files.

Merged 2026-05-21; the Gradle cache is on the default branch. Source

Use it when
Package-manager caches hit while native dependency setup stays cold.
Before applying it
Cache dependency stores and keep native build outputs out of the cache.

Cache Bazel downloads and build outputs

target-determinator's presubmit and release jobs now start with setup-bazel, which caches Bazelisk and downloaded Bazel versions, the external repository cache, and a disk cache for build outputs keyed per tested Bazel version. Pull request runs restore the caches without saving them, so trusted builds on main populate them.

Use it when
Bazel jobs download toolchains and external dependencies and rebuild every output on each run.
Before applying it
Restore-only pull request runs keep untrusted code from writing shared caches; a disk cache per Bazel version keeps versions apart.

Use the ecosystem’s cache integration

Stripe React Native replaced hand-maintained Gradle cache paths with gradle/actions/setup-gradle and built the Android app with --build-cache.

With full caches, the author measured new-architecture app builds at 2:34 against 8:03 and old-architecture builds at 1:42 against 3:50, across all the PR’s changes. Stripe React Native later moved these tests to another CI system in stripe/stripe-react-native#2137. Source

Use it when
Gradle cache maintenance is ad hoc, or builds never reuse cached task outputs.
Before applying it
The source PR changed caching, build flags and emulator ordering together. Measure the cache action on its own, and audit cache paths, trust scope and configuration-cache encryption.

Persist the build cache between runners

Tango added a persistent Bazel disk cache to its integration job. The same PR runs nested queries in a fresh batch-mode Bazel process and raises the outer request timeout. OPT61 is extended from dependency installs to content-addressed build outputs.

The PR reports a local pass in 81s and a successful CI integration job in 6m45s. Source

Use it when
Ephemeral CI jobs repeatedly rebuild the same inputs, or a persistent build process keeps stale state after a checkout changes.
Before applying it
Keep cache validity under the build tool's control and restrict cache writes across trust boundaries. Treat a hung build server as its own problem.
CI technique

Reuse downloaded tools across runs

Cache versioned tools and browser downloads so jobs can restore them while retaining installation and repair checks.

Cache compiled lint tools between runs

Bun cached the cargo-dylint binaries on the tool version, and the dylint driver and mordant library on the version plus the pinned mordant revision. A cache hit skips only the install; the workspace check still runs on every change. The cache carried over when Bun folded its Rust lint jobs into one workflow.

A cold run takes about 6.5 minutes; the PR expects warm runs to take about a minute and a half (estimate). Source

Use it when
Installing or compiling a lint tool takes a large share of the lint job.
Before applying it
The cache key covers the runner OS, the tool version and every pinned revision the tool is built against.

Cache pinned tool and matrix downloads

The same SDK change caches the npm downloads of a version-pinned npx tool and of each smoke-test matrix leg, with keys that name the tool version or the matrix’s Node and TypeScript versions.

Use it when
A pinned command-line tool or matrix leg downloads the same packages on every run.
Before applying it
OPT2’s recipe targets large binaries; here its version-pinned cache key applies to npm package downloads. Key the cache on every version that changes the download, and keep the step that installs or runs the tool.

Reuse browser downloads and keep the repair step

Checkout Kit caches Playwright Chromium in its default Linux cache directory, keyed by the runner's operating system and the web lockfile. The install command still runs after the restore, so it repairs a stale or partial cache and installs system dependencies.

Merged 2026-10-12; the cache step is on the default branch. Source

Use it when
Browser downloads take a large share of a job's setup on every run.
Before applying it
A browser cache can take as long to restore as a fresh download; compare cold, warm and changed-lockfile runs before keeping it.

Reuse a downloaded toolchain

Stripe Android replaced its Java installer with direct Temurin downloads for the configured version, OS and architecture, and reuses an existing install directory on later builds.

Use it when
Every CI job repeats a large toolchain download or installer bootstrap.
Before applying it
Key the reuse on exact version, OS and architecture, verify a published checksum, and install atomically so a partial download is never reused.
CI technique

Persist a tool’s incremental analysis state

Keep reusable configuration or analysis state in addition to dependency downloads, using the tool’s own cache contract.

Persist the tool’s analysis state separately

In the Go SDK a test job could win the shared cache key before lint saved its tools and analysis. Lint now turns off setup-go caching and owns a cache for modules, Go builds and the golangci-lint cache, keyed per revision with restore fallbacks.

Dedicated cache for modules, compiled tools and analysis Source

Use it when
Lint rebuilds its tools or analysis on every run despite restored dependency caches.
Before applying it
Restore, save and storage can outweigh reuse. Separate writers must never publish incomplete state under another job's key.

Let the build tool reuse unchanged work

Shopify CLI's scripts and CI jobs passed --skip-nx-cache, and a cache input included the commit SHA. Shopify/cli#8239 declares explicit cached targets with shared inputs and the real package-local dist outputs, then removes the bypass from scripts and CI.

The PR reports repeated builds and bundles restoring 12 of 12 tasks from cache. Source

Use it when
The build tool supports task caching and the CI commands bypass it.
Before applying it
A cached task whose inputs leave out a file restores stale work; declare shared configuration, lockfiles and tool versions as inputs.

Cache Gradle configuration as well as dependencies

Stripe Android enabled Gradle’s configuration cache with dedicated restore and save steps. A later change also saved Gradle transforms, and publishing commands run with --no-configuration-cache.

The author reports slightly faster builds. Source

Use it when
Gradle spends noticeable time configuring the same task graph on every run.
Before applying it
Configuration caches can contain secrets, so use Gradle’s encryption support and keep cache writes inside a trusted boundary. Resolve configuration-cache warnings before relying on it, and keep incompatible tasks such as publishing on the uncached path.
CI technique

Install prebuilt packages and toolchains

Download compatible prebuilt dependencies instead of compiling the same toolchain or package during every CI run.

Install prebuilt packages

The Python SDK selected NumPy versions that ship wheels for every supported interpreter, so installs download a binary. The same PR restored environment caching and ran only the HTTPX 2 suite in the HTTPX 2 job; the job timings cover all three changes. OpenAI later moved the repository to uv (openai/openai-python#3653).

HTTPX 2 job 10m42s to 2m12s; Python 3.14 job 8m37s to 1m53s Source

Repository moved to uv; the NumPy wheel selection carried into uv.lock Source

Use it when
Install logs show a source build for a dependency that also ships compatible binaries.
Before applying it
Check interpreter, platform and dependency compatibility for every version in the matrix before pinning.

Download the compiler your build depends on

SubmitQueue registered a prebuilt protoc toolchain, so cold builds no longer compile protoc and its C++ dependencies. The same 35 tests passed before and after.

On the author's machine, a cold make test after bazel clean took about 84s before and about 11s after, with the same 35 tests passing. Source

Use it when
Cold runs spend substantial time compiling an upstream tool that has a compatible, trusted binary release.
Before applying it
Verify platform support, compiler compatibility, checksums and generated output, and keep a source build for platforms without a binary release.
CI technique

Keep unrelated environment changes out of cache keys

Include inputs that can change the output while excluding branch identity and unrelated environment churn.

Keep runtime API keys out of unrelated build hashes

The build task stopped hashing RAPID_API_KEY and ANTHROPIC_API_KEY, two task-level env entries. The transferable step is proving that the build reads no runtime-only credentials.

Use it when
Task hashes change with secrets that are consumed only when the application runs.
Before applying it
Keep credentials used by build-time code generation or embedded configuration. Review secret availability separately from cache hashing.

Remove branch identity from content cache keys

A branch-name global environment variable caused different keys between Prebuild and workflow_run consumers. Mastra removed this artificial cache scope.

Use it when
Identical source and tool inputs miss cache when jobs see different GitHub event environments.
Before applying it
Only remove variables proven irrelevant to output. Cache correctness still requires all output-affecting configuration and secure write permissions.

Remove unrelated environment churn from cache inputs

Codex traced different remote cache keys for the same source tree to an unrelated tool version in the hosted Windows PATH. It now computes an explicit PATH in PowerShell and passes it to all three Bazel environments, keeping the tools the build uses.

Bazel actions see one explicit PATH across all three environments Source

Use it when
Identical source trees miss remote build caches after a runner image update.
Before applying it
A PATH allow-list can hide a required tool. Keep real tool versions in the cache inputs so a toolchain change still invalidates results.
CI technique

Persist compiler output between runs

Restore compatible compiler outputs and intermediate objects instead of recompiling unchanged inputs.

Cache the cross-compile producer’s objects

Codex's archive refactor moved the Windows ARM64 build onto a Windows x64 producer and turned on sccache for it, keying the producer's caches by the archive runner so the cross-compile reuses the Windows x64 cache.

sccache on for the Windows ARM64 archive producer Source

Use it when
One producer job compiles the same Rust or C++ inputs on every run before handing test binaries to native runners.
Before applying it
Compiler caches need the toolchain, target and flags in their identity, and trusted writers only. Cached compilation leaves native test execution in place.

Persist the compiler cache and warm it from the default branch

Ramp's CLI release workflow compiles the Python CLI to native binaries with Nuitka on every version tag. Ramp rooted Nuitka's caches (ccache on Linux and macOS, clcache on Windows) in the workspace through NUITKA_CACHE_DIR and saved them with actions/cache, keyed on the lockfile with restore prefixes. Caches are scoped per ref, so a new release tag starts from the default branch's caches and shares nothing with other tags; the workflow therefore also runs on every push to main to warm the cache; those runs build but skip packaging, artifact upload and the release jobs. A rerun of the same tag can also reuse that tag's own cache.

In the commit Release v0.2.26, the release workflow restores the Nuitka compiler cache on every build and warms it on each push to main. Source

The commit's workflow comments describe the change; the quote is from the diff. GitHub's cache rules let a run restore caches from its own ref and the default branch, so the quoted comment is narrower than that rule.

Use it when
A build job compiles the same native outputs from scratch on every run, and release builds run on tags.
Before applying it
A new tag build starts from the default branch's caches, so the warming run on main must use the same cache key prefix as the tag build.
CI technique

Reuse Docker build layers across runners

Restore image layers from a persistent cache so new runners can avoid rebuilding unchanged stages.

Restore image layers from the registry cache

Confidence's CI build on pushes to main writes its Docker layer cache to the container registry. The change builds the release deployer image from a target in the shared Dockerfile and adds cache-from for that registry cache, so the release build restores the layers CI already built.

Use it when
One workflow builds images with a layer cache, and another builds from the same Dockerfile without one.
Before applying it
Release builds read the cache that default-branch CI writes, so keep write access to trusted builds.

Persist Docker build layers across runners

Stripe sync-engine added BuildKit registry caches for its image builds, separated by architecture, so the dependency install layer is reused and only the source copy layer rebuilds.

Use it when
Container dependency layers rebuild on every ephemeral runner.
Before applying it
Keep registry credentials and cache writes inside the trusted boundary, and verify cold and warm builds produce equivalent images.
CI technique

Bake stable dependencies into runner images

Prepare versioned system dependencies and build downloads once in the runner image instead of installing them on every job.

Bake versioned build downloads into the runner image

Bun bakes a read-only prefetch store of its dependency tarballs, WebKit prebuilt and zig compiler into its build images. A build copies a prefetched file when its URL matches and downloads from the network when a version has changed.

Before the change every CI build downloaded about 18 archive tarballs, a 200 MB WebKit prebuilt and a zig compiler before compiling. Source

Use it when
Builds on fresh runners download the same large, versioned inputs on every run, and you control the runner image.
Before applying it
Entries are keyed by exact URL or version, so a bumped dependency downloads fresh until the image is refreshed.
CI technique

Cache the directories the build actually uses

Align restored and saved cache paths with the tool’s real output directories so the next run can use the cached work.

Cache the paths the build actually writes

SubmitQueue originally cached ~/.cache/bazel while its build wrote separate disk and repository caches. The correction saves those actual paths, gives each job its own cache entries, and adds a per-run suffix with stable restore prefixes so the content-addressed cache can grow. OPT61 is adapted to repairing an existing cache.

The author links a first run of about 10m and a second run of 1m24s; the same PR also changes application and test code. Source

Use it when
Cache steps report success but warm runs still download dependencies or rebuild unchanged targets.
Before applying it
Confirm the paths hold reusable inputs and outputs. The run-specific save key suits a content-addressed cache with stable restore prefixes. Measure restore and upload time, eviction and cold misses, and restrict cache writes across trust boundaries.
CI technique

Declare the inputs and outputs of cached builds

Make build-cache contracts cover the actual files and configuration consumed and produced by the task.

Make build cache keys describe the build

Mastra declared source, package and compiler configuration inputs for library builds, excluding test files and Markdown where they were not build inputs. Composite tasks received their own input declarations.

Use it when
A remote build cache misses after edits that leave the emitted artifact unchanged.
Before applying it
Exclusions must match actual tools, generators and imported assets.
CI technique

Key nested installs from their own lockfile

A checkout in a subdirectory needs a cache key for that dependency graph.

Key nested installs from their own lockfile

The shared setup action added a cache-dependency-path input, and trusted nested checkouts passed their own pnpm-lock.yaml. This adapts cache-key granularity to checkout layout.

Use it when
A shared setup action serves multiple working directories or independent dependency graphs.
Before applying it
Installation still runs after a cache hit. Preserve frozen lockfiles, platform and package-manager compatibility, and trusted/untrusted cache separation.
CI technique

Prepare shared test inputs once

Produce or download common test inputs once and pass them to the jobs that consume them.

Publish test packages once and hand off the registry

Mastra introduced a producer job that publishes required local packages to Verdaccio storage, uploads it, and lets each consumer job download and serve that snapshot.

Use it when
Independent fixture tests repeat the same package publishing and registry preparation.
Before applying it
The producer becomes a shared dependency. Compare its latency plus artifact transfer against repeated work; bind artifacts to the same commit and platform, preserve trust and never fall back silently to public packages.
CI technique

Reduce the setup floor before increasing shard count

Each extra shard repeats checkout, dependencies and container setup.

Reduce the setup floor before increasing shard count

Each extra shard repeats checkout, dependencies and container setup. Before its setup work, Linear’s test job used 4 shards and spent 8.3 minutes on setup; afterward it ran 8 shards with 7.5 minutes of setup.

Use it when
More shards would repeat substantial fixed setup work.
Before applying it
Remeasure per-shard setup before changing shard count.
CI technique

Remove caches that cost more than they save

Measure cache restore and upload time against the work avoided. Remove caches whose transfer cost outweighs their benefit.

Remove a cache when restoring it costs more than rebuilding

Linear measured a node_modules cache hit at about 28 seconds to restore, against roughly 7.5 seconds for a filtered install. The cache key depended on a frequently changing lockfile, so Linear removed the cache.

Use it when
Comparable measurements show restore and save overhead outweighing the work avoided.
Before applying it
First try a package-manager store or a narrower cache, and separate miss-rate problems from an expensive successful restore.
CI technique

Restore compatible Python environments

Restore the resolved virtual environment and keep the install step after it.

Restore compatible Python environments

The Python SDK added a virtual-environment cache to three jobs, keyed by operating system, architecture, resolved Python version and both lockfiles, with dependency sync still running after the restore. The repository now uses uv's built-in cache (openai/openai-python#3653).

Environment cache keyed by OS, architecture, Python version and lockfiles Source

Repository moved to uv, with uv's built-in cache in CI Source

Use it when
Dependency installation repeats on fresh CI runners despite unchanged interpreter and lockfiles.
Before applying it
Virtual environments can hold interpreter-specific paths and binaries. Key on everything that changes them, and keep the install step so a stale cache is repaired.
CI technique

Restore dependencies after their manifest exists

Check out the code before the setup action, so its dependency cache can find the manifest.

Restore dependencies after their manifest exists

TChannel's Go setup ran before checkout, and checkout used a nested GOPATH directory. The fix runs checkout first, points setup-go at **/go.sum and removes an obsolete Glide cache. OPT61 is adapted to repairing cache discovery.

The merged diff moves checkout ahead of setup-go and sets cache-dependency-path to the real go.sum location. Source

Use it when
A language setup action runs before checkout, or looks for the dependency manifest in the wrong directory under a custom checkout path.
Before applying it
Use a reviewed action version and a manifest glob that matches only the intended modules, and keep dependency installation and integrity checks on cache hits.
CI technique

Reuse a snapshot of the build store

Restore the newest store snapshot for the dependency inputs, and save a new one after the component tests.

Reuse a snapshot of the build store

Most of a Tecnix run rebuilt third-party dependencies that change only with flake.lock and packaging/dependencies.nix. Shopify/tecnix#74 caches the Nix store as one snapshot per build variant, keyed by a hash of those files plus the commit, restores the newest snapshot with that prefix, and saves only from this repository's main pushes, pull requests and manually dispatched runs.

Median PR run over 2026-08-10 to 2026-09-29: 61.8 minutes. After the PR, which makes seven changes (among them the cache, pull requests running only the component tests and the cheap flake checks, and separate sanitizer and coverage jobs), with a 20 to 30% hardware spread measured between runs, a later push with a cache hit and no source change ran 4.7 minutes. One sample per row, as the PR reports. Source

Use it when
Fresh runners rebuild the same dependency closure on every run.
Before applying it
The three snapshots used about 4.3 GB of the 10 GB repository cache quota; restore took 0.5 to 2 minutes and save 0.3 to 1.6.
CI technique

Stop downloading the test image on every build

Cache the container image the tests run in, so each build reuses the copy it already has.

Stop downloading the test image on every build

Each build pulled the last-built Docker image from the registry, which took 1 to 2 minutes. Ramp's new test runners were self-hosted, so the image stayed in the runner's local Docker cache between builds. On GitHub-hosted runners, which start empty, a job that builds the image may cut the same overhead with a persisted build cache (the OPT4 recipe), and a job that only pulls a prebuilt public image may cut it by saving and loading the image through the Actions cache; measure the restore time against the pull it replaces.

Local Docker caching shaved over a minute off a run that took about fifteen minutes by then. Source

Use it when
Jobs pull or rebuild the same container image on every run and spend a minute or more on it.
Before applying it
A cached image must still rebuild when its inputs change, so key the cache on the Dockerfile and the lockfiles it copies in.
CI technique

Stop writing caches that no later run restores

Remove cache writes and archives that have no consumer, eliminating work without changing the build result.

Turn off a build cache that CI never restores

Next.js caches Webpack, ESLint and SWC under .next/cache by default, and for large projects the Webpack cache can exceed 5 to 10 GB. CI jobs never saved that cache between runs, so every write was wasted disk activity. Disabling the Webpack disk cache in CI saved three minutes per run.

Use it when
A build tool writes a large local cache on every CI run, and no cache step saves or restores that directory.
Before applying it
Keep the cache wherever a later run restores it (developer machines, or CI with a cache step whose hit rate you have measured).

Company Playbooks