Do Caches Carry Across Branches?

Caches carry in one direction. A run reads entries from its own branch and from the default branch, so a feature branch starts from whatever main last wrote.

Caches carry across branches in one direction. A workflow run can restore entries written on its own Git ref and entries written on the repository's default branch, and nothing else, so a brand new feature branch starts from whatever the last run on main wrote, while two sibling feature branches are invisible to each other.

Answer

The restore path is short and worth memorizing. GitHub documents it under restrictions for accessing a cache in the dependency caching reference, checked on 2026-08-13.

  • A run searches its own ref first. A push to feature-a records refs/heads/feature-a, a pull request run records refs/pull/<number>/merge, and a tag run records refs/tags/<tag>.
  • It then searches the default branch, usually refs/heads/main.
  • A pull request run also reaches the base branch it targets, including when the pull request arrives from a fork.
  • The search stops there. No other ref is consulted, and no other repository is consulted.

Scope is checked before the key. An entry whose key matches your request byte for byte still returns a miss when it sits outside the searched scopes, and the log reports an ordinary cache miss with no indication that a matching entry exists elsewhere.

Three branches, and what each one can read

Take a repository whose default branch is main, with two feature branches, and a pull request opened from feature-b into main. Each row is a run; each column is a set of entries the run may read.

RunEntries on refs/heads/mainEntries on refs/heads/feature-aEntries on refs/heads/feature-bEntries on refs/pull/318/merge
Push to mainReadableOut of scopeOut of scopeOut of scope
Push to feature-aReadableReadableOut of scopeOut of scope
Push to feature-bReadableOut of scopeReadableOut of scope
Pull request 318 from feature-bReadable, as the base branchOut of scopeOut of scopeReadable

Three consequences fall out of that grid.

Every run reads main. That column is the only one with a mark in every row, which makes the default branch the single place worth seeding.

A branch cannot borrow from a sibling. feature-a and feature-b may install the identical dependency set from the identical lockfile, and each pays for its own install and stores its own copy.

A pull request and a push on the same branch are separate scopes. Pushing to feature-b writes refs/heads/feature-b, while the pull request run writes refs/pull/318/merge, so the two event types on one branch keep two sets of entries and neither refreshes the other. The cache scope definition works through the ref identity in more detail.

WarpBuild Cache follows the same model. WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 with the same key, path, and restore-keys inputs, and the entry address is the key, the version hash, and the branch, per the WarpBuild caching documentation. The cache is enabled by default on Linux runners.

Detail

The branch rule decides where restore-keys are useful

restore-keys widens the key match, and it does nothing to widen the scope. A prefix such as deps-Linux-X64- searches for the most recent matching entry inside the refs the run may already read, which means a feature branch falling back on a prefix is almost always landing on an entry written by main.

That is the useful behavior, and it sets the quality bar. When the entry on main is a week stale, every feature branch in the repository restores a week-stale tree and pays the reconcile cost. The entry on the default branch is shared infrastructure, and its freshness is the number that moves branch job durations.

Pull request runs cannot fix this themselves. A pull request run writes into its own merge ref, so however good the tree it builds, the next branch that starts fresh will never see it. Only a run on the default branch can refresh what everyone reads.

Warm the default branch on a schedule

The pattern that follows from the grid is a small workflow that runs on the default branch on a timer, restores the shared entry, brings it up to date, and saves it back. Feature branches then inherit a current entry on their first run instead of a stale one.

Two GitHub behaviors make the schedule trigger the right fit. Scheduled workflows run only on the default branch, so the entry lands in the scope every branch can read without any branch conditionals. Scheduled workflows are also disabled automatically after 60 days of repository inactivity, per the events that trigger workflows reference, so add workflow_dispatch for a manual restart.

name: warm-cache

on:
  schedule:
    - cron: "17 4 * * *"
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: warm-cache
  cancel-in-progress: true

jobs:
  warm:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v5

      - name: Restore dependency cache
        id: deps
        uses: WarpBuilds/cache/restore@v1
        with:
          path: |
            node_modules
            .cache/turbo
          key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('bun.lock') }}
          restore-keys: |
            deps-${{ runner.os }}-${{ runner.arch }}-

      - run: bun install --frozen-lockfile

      - run: bun run build

      - name: Save dependency cache
        if: steps.deps.outputs.cache-hit != 'true'
        uses: WarpBuilds/cache/save@v1
        with:
          path: |
            node_modules
            .cache/turbo
          key: deps-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('bun.lock') }}

Four details carry the design.

The cron minute is 17 rather than 0. GitHub delays scheduled workflows during periods of high load, and names the start of every hour as one of those windows, per the events that trigger workflows reference, so an off-peak minute reduces the drift between the schedule and the actual start.

The save step is guarded on cache-hit. An exact hit means the entry already exists under that key and a second write would be rejected, so on a quiet day the run restores, confirms, and exits. That restore is what matters for lifetime: WarpBuild Cache entries expire 7 days after their last use, per the caching documentation, and a restore counts as a use, so a daily schedule keeps the shared entry from aging out during a slow week.

The push trigger sits alongside the schedule so that a merged dependency bump refreshes the entry immediately rather than waiting for the next timer tick.

The concurrency group prevents two warming runs from racing when a merge lands close to a scheduled tick. Both would compute the same key and one would waste its runner minutes losing the write.

When the shared content is a language toolchain or a package manager directory, the key and the path list can come from a setup action instead of from your workflow file. WarpBuilds/setup-node@v6, WarpBuilds/setup-go@v6, and the rest of the list in the setup actions documentation compute both, which removes the risk of the warming job and the branch jobs drifting to different path lists and therefore different version hashes.

What the warming job costs

Rates below are from the pricing page, checked on 2026-08-13. The assumptions are stated so you can substitute your own: one warming run per day for 30 days on warp-ubuntu-latest-x64-4x at 4 minutes per run, one restore and one save operation per run, and a 2.5 GB entry held on the default branch for the whole month.

LineMonthly quantityRateCost
Warming runner minutes120 minutes$0.008 per minute$0.96
Cache operations60 operations$0.0001 per operation$0.01
Shared entry on the default branch2.5 GB-months$0.20 per GB-month$0.50
Total$1.47

Set that against the branch side. At 600 feature branch runs in the month, each skipping 2 minutes of dependency installation on the same runner size, the repository avoids 1,200 runner minutes, which is $9.60 of runner billing at $0.008 per minute. The sensitive assumption is the install time a warm entry actually removes: below about 20 seconds per run, the warming job costs more than it returns, and the shared entry is better left to refresh on merges alone. On BYOC runners, cache storage and cache operations are both free, so only the warming runner minutes remain.

Two structural notes belong with any of these numbers. The persistent caches guide covers the cases where whole-machine state beats path-level caching entirely.

Can a pull request from a fork restore my repository's cache?

A pull request run can restore entries written on the base branch, including when the pull request comes from a fork. It cannot write into the base branch scope. Entries a fork pull request saves land on its own merge ref and are readable only by later runs on that same pull request, which is the isolation boundary that keeps an untrusted contributor from poisoning the entry every branch reads. The cache scope definition lists the ref each event type records.

Why does my feature branch miss an entry another feature branch just wrote?

Two feature branches are two separate scopes. A restore request searches the run's own ref and then the default branch, and stops there, so an entry on refs/heads/feature-a is invisible to a run on feature-b even when the key matches exactly. Seed the shared entry from the default branch instead, using the warming workflow above. Can I share a cache between GitHub Actions workflows covers the key and version half of the same match.

How often should the default-branch warming job run?

More often than the expiry window. WarpBuild Cache entries expire 7 days after their last use, and a restore counts as a use, so a daily or every-other-day schedule keeps the shared entry alive and current. Scheduled workflows run only on the default branch, which puts the entry in the scope every branch can read. The persistent caches guide walks through the cadence against entry size.

What if the branch boundary is not the problem, and the repository boundary is?

Nothing in the cache service crosses repositories. An entry belongs to one repository and a workflow elsewhere is refused whatever key it presents, so a monorepo split into several repositories cannot share one dependency cache through this mechanism. Moving the storage to a remote build cache the build tool talks to directly is the usual answer, and the guide on sharing a build cache across repositories covers the options.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.