- All playbooks
- Caching and setup
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
- Read Anthropic’s playbookRun only affected tests
- Read DoorDash’s playbookRun build and test in parallel, sharded across CI agents
- Read Linear’s playbookBenchmark the same job on faster hardware
- Read Mastra’s playbookStarSling runners for the slowest test workflow
- Read OpenAI’s playbookRank CI fixes by the minutes they save
- Read Ramp’s playbookMove a CPU-bound test suite to a faster CPU
- Read Shopify’s playbookLet an agent iterate against one CI metric
- Read Spotify’s playbookBuild arm64 on native arm64 runners
- Read Stripe’s playbookRun only the tests a change can reach
- Read Uber’s playbookBuild the merge-queue speculations most likely to be needed