How Do I Cache Playwright Browsers?
Cache the Playwright browser download directory with a key built from the installed Playwright version, and install the system libraries in a separate step.
Cache the directory Playwright downloads browsers into, and build the key from the installed Playwright version rather than from a lockfile hash, because every browser build is pinned to the release that downloaded it. Keep the system library install in its own step, so a cache hit skips the browser download without skipping the apt packages those browsers link against.
Answer
Playwright writes browser builds to a shared download directory outside the checkout, and the path depends on the operating system (Playwright browsers reference).
| Runner platform | Browser download directory | Cache action to use |
|---|---|---|
| Linux | ~/.cache/ms-playwright | WarpBuilds/cache or actions/cache@v4 |
| macOS | ~/Library/Caches/ms-playwright | actions/cache@v4 |
| Windows | %USERPROFILE%\AppData\Local\ms-playwright | actions/cache@v4 |
The WarpBuild cache is a Linux runner feature covering Linux x64 and Linux ARM64 (caching documentation and the cloud runners reference, checked on 2026-08-13). Browser suites belong on Linux x64 anyway, which is where the browser builds and their system packages are best supported, so the Linux row is the one most workflows configure.
The key carries the Playwright version, read out of the installed package rather than guessed:
name: e2e
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: WarpBuilds/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- name: Resolve the installed Playwright version
id: pw
run: |
echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
- name: Restore browser binaries
id: browsers
uses: WarpBuilds/cache@v1
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.pw.outputs.version }}
- name: Install browsers and system libraries
if: steps.browsers.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium firefox webkit
- name: Install system libraries only
if: steps.browsers.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium firefox webkit
- run: npx playwright testThree details in that file do the work. The version comes from @playwright/test/package.json after npm ci, so it reflects what the job actually installed. The browser entry carries no restore-keys, so it either hits exactly or misses. And the browser download and the system library install are separate steps with opposite conditions, which is what lets a hit skip the download and still leave the runner able to launch a browser.
WarpBuilds/cache@v1 is a drop-in replacement for actions/cache@v4 and takes the same path, key, restore-keys, and cache-hit inputs and outputs, so the swap is the uses line (caching documentation). WarpBuilds/setup-node is the matching drop-in for actions/setup-node and routes the npm dependency cache through the same backend with no extra configuration (setup actions documentation).
That job runs on warp-ubuntu-latest-x64-4x at 4 vCPU, 16 GB and $0.008 per minute (cloud runners reference, checked on 2026-08-13).
Detail
Why the key hashes the version and not the lockfile
Playwright downloads a specific browser revision per release and stores it under a revision-numbered directory such as chromium-1148. The mapping from Playwright version to browser revision is fixed by the release, which makes the version the only input that decides whether a stored browser set is still the right one.
A lockfile hash gets both directions wrong. It changes when an unrelated dependency moves, which rolls the key and discards browsers that were still correct, so the next job downloads a full set for nothing. It can also stay unchanged across a Playwright bump applied outside the lockfile you hashed, in a monorepo workspace or a container image, which hands the job browsers that the new release will not use.
Reading the version at run time also survives version ranges. A package.json that declares ^1.56.0 resolves to whatever the lockfile pinned, and the step output reports the resolved value rather than the range. The general shape of a key, and what each segment is for, is in the cache key definition.
Two behaviors of the cache itself matter once the key is right. Entries are scoped to the key, the cache version, and the branch, so seeding the entry on your default branch is what makes pull request jobs hit. The cache version is a hash over the compression tool and the cached paths, which means an entry written on a macOS runner cannot restore on a Linux runner even with an identical key (caching documentation). Entries expire 7 days after last use, so a Playwright version you stopped installing stops costing storage on its own.
System libraries live outside the cached path
npx playwright install --with-deps does two jobs: it downloads browser builds into the download directory, and it installs the operating system packages those builds link against through the platform package manager. Only the first job writes inside ~/.cache/ms-playwright.
Combining both into one step behind if: cache-hit != 'true' is the most common way this configuration breaks. The restore succeeds, the combined step is skipped, and the first browser.launch() fails on a missing shared library rather than on a missing executable. Splitting the step, as in the workflow above, keeps the download conditional and the library install unconditional.
An alternative is to move the download directory into the workspace with PLAYWRIGHT_BROWSERS_PATH, which makes the cached path identical across operating systems (Playwright browsers reference). It does not change the library problem, and it does not let a Linux entry restore into a macOS job, because the cache version still differs by platform.
The mismatch failure, and how it reads in a log
The failure to plan for is a restored browser set that does not match the installed Playwright version. It has one visible symptom:
Executable doesn't exist at /home/runner/.cache/ms-playwright/chromium-1148/chrome-linux/chrome
Looks like Playwright Test or Playwright was just installed or updated.
Please run the following command to download new browsers:
npx playwright installThree configurations produce it. A restore-keys prefix on the browser entry restores the newest entry with a matching prefix, which after an upgrade is the previous version's browser set. A key built from the lockfile hash stays stable across a Playwright bump that changed the required revision. And a version read from a checked-in file rather than from the installed package reports whatever that file declares, which drifts from the release npm ci actually resolved.
The fix in all three cases is the same pair: a key that changes exactly when the Playwright version changes, and no restore-keys on that entry. A miss costs one download. A wrong hit costs a failed run plus the rerun.
When a restore looks correct in the log and the launch still fails, the Action Debugger opens an SSH session on the runner so you can list the revision directories under ~/.cache/ms-playwright and compare them against the installed release. CI observability reports the per-step durations that show whether the install step is running cold across consecutive runs.
What the cache is worth on a sharded suite
The saving scales with the shard count, because every shard in the matrix downloads its own copy without a cache. Measure your own install step first, then run the arithmetic with that number as the input.
Take an eight-shard matrix on warp-ubuntu-latest-x64-4x at $0.008 per minute, and use one minute of browser download per cold shard as the input. That is 8 x 1 x $0.008 = $0.064 of download time per run, or $25.60 across 400 runs a month. The cache side of the ledger is $0.20 per GB-month of storage plus $0.0001 per write or restore operation, so a 1 GB browser entry restored by eight shards on 400 runs costs $0.20 in storage and $0.32 in operations (pricing page, checked on 2026-08-13). Substitute your own measured download time and entry size before deciding.
Caching browsers is one of several mechanisms for carrying state between runs, and the trade-offs against snapshot runners for state that does not fit a cacheable path are covered in persistent caches for GitHub Actions runs.
Related Questions
Should the Playwright browser cache key hash the lockfile?
No. Browser builds are pinned to the Playwright release, so the key should carry the installed Playwright version. A lockfile hash rolls the entry on every unrelated dependency bump and throws away browsers that were still correct, while leaving the key unchanged on a Playwright upgrade that does need new binaries. The segment-by-segment reasoning behind a key like this is in cache key, defined.
Do I still need install-deps when the browser cache hits?
Yes. The cache covers the browser binaries under the download directory. The apt packages the browsers link against are installed into system directories outside that path, so a restored entry gives you browsers with no libraries. Run npx playwright install-deps on the hit branch and npx playwright install --with-deps on the miss branch.
Why does my job fail with "Executable doesn't exist" after a Playwright upgrade?
Playwright stores each browser under a revision-numbered directory, and every release expects its own revision. A restored entry from the previous version has the old revision directory and not the new one, so the launch fails. The fix is a key that changes with the Playwright version and no restore-keys on that entry.
Where does the browser cache fit in a full Playwright pipeline?
It sits between dependency install and the test run, and it pairs with sharding, blob report merging, and runner sizing. Playwright test suites on GitHub Actions covers the full pipeline shape, and running browser tests headless in GitHub Actions covers the display and launch flags those jobs need. For state that no cache action can carry, compare the mechanisms in persistent caches for GitHub Actions runs.
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.