Cypress Test Suites on GitHub Actions
Cache the Cypress binary folder with the npm store on one key, split specs across a warp- runner matrix, and size headless Chrome jobs at 4 vCPU or more.
Last verified:
Overview
To run Cypress tests on GitHub Actions, restore the Cypress binary folder and the npm store from one cache key, split the spec files across a matrix of jobs, and give each job at least 4 vCPU so headless Chrome has room to work. A default single-job setup downloads the Cypress binary on every run and then walks the whole spec list serially in one browser, which is what turns a ten-minute suite into a thirty-minute pull request gate.
Cypress holds more open at once than most test runners. A single cypress run invocation keeps the Cypress Node process alive, plus a full browser process tree: a renderer for the test window, a renderer for the application under test, and the browser's own utility and networking processes. Most projects also serve the application from the same machine, and when video recording is enabled ffmpeg runs on that machine too. All of it shares one runner's memory budget.
Cypress work belongs on the Linux x64 sizes. Those images carry the same tooling as GitHub-hosted runners, so Chrome is already installed and --browser chrome works without an install step, and WarpBuild Cache is enabled by default on Linux runners, which is where the binary cache lands. Moving an existing job across is a one-line change to runs-on.
One boundary before the configuration. This page covers Cypress: its separate binary cache, its spec-level parallelism, and its browser memory profile. A different browser automation stack has a different worker model and a different download path, and that configuration lives on the Playwright test suites on GitHub Actions page.
Configuration
Two directories need caching and they sit in different places. npm ci rebuilds node_modules from tarballs in the npm store at ~/.npm. The Cypress binary is not in node_modules at all: the cypress package postinstall downloads a platform-specific binary into ~/.cache/Cypress/<version>, and the npm package shells out to it. Cache one without the other and the job either refetches every package or fails at cypress run with a message about no version of Cypress being installed.
Put both paths on one cache entry, keyed on the lockfile so a Cypress version bump rolls the binary and the dependency set together:
name: cypress
on:
push:
branches: [main]
pull_request:
jobs:
build:
runs-on: warp-ubuntu-latest-x64-4x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore npm store and Cypress binary
uses: WarpBuilds/cache@v1
with:
path: |
~/.npm
~/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-cypress-
- run: npm ci
- run: npx cypress install
- run: npx cypress verify
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: app-dist
path: dist
retention-days: 1
e2e:
needs: build
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- shard: 0
runner: warp-ubuntu-latest-x64-4x
- shard: 1
runner: warp-ubuntu-latest-x64-4x
- shard: 2
runner: warp-ubuntu-latest-x64-4x
- shard: 3
runner: warp-ubuntu-latest-x64-8x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore npm store and Cypress binary
uses: WarpBuilds/cache/restore@v1
with:
path: |
~/.npm
~/.cache/Cypress
key: ${{ runner.os }}-cypress-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-cypress-
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: app-dist
path: dist
- name: Select specs for this shard
id: specs
run: |
list=$(find cypress/e2e -name '*.cy.ts' | sort \
| awk "NR % 4 == ${{ matrix.shard }}" | paste -sd,)
echo "list=$list" >> "$GITHUB_OUTPUT"
- name: Run Cypress
run: |
npx cypress run \
--browser chrome \
--spec "${{ steps.specs.outputs.list }}"The build job is what warms the cache. npx cypress install is a no-op when the binary is already in the restored folder and a download when it is not, so the step is safe to leave in permanently and it gives you one log line that tells you whether the cache hit. npx cypress verify fails fast on a corrupted or partially restored binary instead of letting four shards each hit the same error twenty minutes later.
The shards use WarpBuilds/cache/restore@v1 rather than the combined action so that four jobs do not race to write the same key. fail-fast: false keeps a single broken shard from cancelling the other three, which matters when you want the full failure list from one run instead of one failure at a time.
Two cache behaviors from the caching documentation shape how this performs. The cache is scoped to key, version, and branch, so seed it on main and let branch builds fall back through restore-keys. Entries expire 7 days after last use, so an active repository stays warm on its own. Cache usage is metered at $0.20 per GB-month of storage and $0.0001 per write or restore operation, which for a Cypress repository with a 2GB entry and 8,000 operations a month comes to roughly $1.20.
If your Cypress job runs inside a container:, HOME differs from the runner's home directory and the binary lands somewhere the cache paths above do not cover. Set CYPRESS_CACHE_FOLDER to an explicit path inside the workspace and cache that path instead. Keep Cypress caching on Linux jobs: WarpBuild Cache is not supported on Windows runners.
Sizing
WarpBuild Linux x64 runners scale from 2 to 32 vCPU with 4GB of memory per core. These are the sizes that matter for Cypress, with the nearest GitHub-hosted shape and its published list price beside each one:
| Runner label | vCPU | Memory | Storage | Per minute | Nearest GitHub-hosted shape | GitHub list price |
|---|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | 2 | 8GB | 150GB SSD | $0.004 | ubuntu-latest, 2 vCPU and 8 GB on private repositories | $0.006 |
| warp-ubuntu-latest-x64-4x | 4 | 16GB | 150GB SSD | $0.008 | 4-core Linux larger runner | $0.012 |
| warp-ubuntu-latest-x64-8x | 8 | 32GB | 150GB SSD | $0.016 | 8-core Linux larger runner | $0.022 |
| warp-ubuntu-latest-x64-16x | 16 | 64GB | 150GB SSD | $0.032 | 16-core Linux larger runner | $0.042 |
WarpBuild rates come from the cloud runners documentation. GitHub list prices come from the GitHub Actions minute multipliers reference, checked on 2026-08-13.
The 2x size is where headless Chrome runs into trouble. Eight gigabytes has to cover the Cypress Node process, two renderer processes, the browser's supporting processes, and the application under test, and Cypress keeps DOM snapshots for its time travel debugger in memory as a spec progresses. On Linux, /dev/shm is a tmpfs sized from system memory, and Chrome shares memory between renderers through it, so a smaller runner lowers that ceiling as well as the total. The failure mode is a renderer crash mid-spec with a message about the browser process closing unexpectedly, which reads like a flaky test and is a resource problem.
That makes warp-ubuntu-latest-x64-4x at 4 vCPU and 16GB the practical floor for a Cypress shard, at $0.008 per minute. Move a shard to warp-ubuntu-latest-x64-8x at 32GB when the job also starts a dev server and a database in service containers, when specs are long enough that snapshot accumulation matters, or when video recording is on and ffmpeg competes with the browser for cores.
Buying beyond 8x for a single Cypress job rarely pays. cypress run works through its spec list one file at a time in a single browser instance, so there is no in-process worker pool to feed. A 16 vCPU runner leaves most of those cores idle for the length of the run. The lever that uses more cores is the matrix: four shards on 4x finish in roughly the wall time of the slowest shard while spending about the same total minutes as one serial job on 4x. The mechanics of splitting and fanning out are covered in the GitHub Actions matrix builds guide.
Worked cost model
Take a suite of 240 spec files that runs 48 minutes end to end on one machine. Split four ways it becomes four shards of about 13 minutes each, with a 4-minute build job in front of them. Three shards sit on 4x and the slowest shard sits on 8x. The team merges enough to trigger 600 workflow runs a month.
| Line item | Rate | Minutes per month | Monthly cost |
|---|---|---|---|
| Build job plus 3 shards, 4 vCPU (43 min per run) | $0.008/min | 25,800 | $206.40 |
| Heavy shard, 8 vCPU (13 min per run) | $0.016/min | 7,800 | $124.80 |
| WarpBuild total | 33,600 | $331.20 | |
| Same minutes on the 4-core Linux larger runner | $0.012/min | 25,800 | $309.60 |
| Same minutes on the 8-core Linux larger runner | $0.022/min | 7,800 | $171.60 |
| GitHub-hosted total | 33,600 | $481.20 |
The two per-size comparisons behind that table, stated as list-price arithmetic: warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) costs $0.008 per minute against $0.012 per minute for the 4-core Linux larger runner (4 vCPU, 16 GB), which is 33 percent lower list price, and warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB), which is 27 percent lower list price. GitHub list prices checked on 2026-08-13.
Full rates for every size and platform are on the pricing page.
Bottlenecks
Four things account for most slow Cypress pipelines on GitHub Actions.
Binary download per run. The Cypress binary is fetched once per cold machine, and every matrix shard is a cold machine. A four-shard suite that misses the binary cache pays that download four times per run before a single test executes. Diagnose it from the install log: a warm run reports the binary as already installed at the cached path, and a cold run shows a download with a progress bar. The fix is the two-path cache entry above, and the reason it gets missed is that people cache the dependency store, see install times drop, and never notice the separate binary fetch underneath.
Single-spec serialization. One cypress run process executes specs in sequence. Wall time equals the sum of every spec, no matter how large the runner, so a suite that grows by ten specs a month gets steadily slower with no configuration change to blame. Splitting across a matrix is the only lever that changes the shape of that curve. Round-robin splitting by file count, as in the workflow above, is coarse: if one spec takes 6 minutes and the rest take 20 seconds, the shard holding it sets the wall time. When that happens, pin the slow spec to its own shard by name and give that shard a larger label, which is what the include block in the matrix is for.
Memory exhaustion on small runners. Covered under sizing, and worth naming here because it presents as flakiness rather than as an out-of-memory error. Symptoms are renderers closing unexpectedly, specs that pass alone and fail in a full run, and failures that move around between runs. Before rewriting the tests, set numTestsKeptInMemory lower than its default of 50 so old DOM snapshots stop accumulating, turn on experimentalMemoryManagement for Chromium, and move the shard to 8x. If the job runs in a container, raise its shared memory: Docker defaults /dev/shm to 64MB, which is well under what Chrome expects.
Video encoding. Cypress encodes one video per spec with a bundled ffmpeg, and compression is CPU work that lands at the end of each spec. On a 4 vCPU shard that encode competes with the next spec's browser startup. Check the video key in your cypress.config.js before assuming it is off, since a config carried forward from an older major version may still record everything. Keep recording on only for the shards you actually debug from, or set it to record on failure and drop videoCompression so the file is written without the encode pass.
When a shard is slow and the cause is unclear, WarpBuild's CI observability shows OpenTelemetry-based system metrics from the runner agent correlated with GitHub Actions job logs, which separates a memory-bound browser from a spec that is genuinely waiting on the network. For a live look, the Action Debugger pauses the workflow and opens an SSH session on the runner, so you can run npx cypress cache path, list the cached versions, and confirm what the restore actually put on disk. Dependency-side caching questions are answered separately in how to cache node modules in GitHub Actions and in the Node.js builds on GitHub Actions page.
Proof
For a Cypress suite the compounding effect sits in the shards, since each one pays its own checkout, install, and browser startup.
Public OSS repositories running warp- labels are citable evidence, and you can check them yourself by opening the workflow file and reading the runs-on line. The Trigger.dev end-to-end suite runs its matrix on warp-ubuntu-latest-x64-4x and warp-windows-latest-x64-8x, which you can read in triggerdotdev/trigger.dev's e2e.yml (checked on 2026-08-13).
FAQ
Why does Cypress download its binary on every GitHub Actions run?
The cypress npm package is a thin client. Its postinstall script downloads a platform binary into ~/.cache/Cypress, which sits outside node_modules, so a cache that only covers the npm store or node_modules never restores it. Add ~/.cache/Cypress to the same cache entry.
Which directories does a Cypress job need to cache?
Two: the package manager store at ~/.npm for dependencies, and ~/.cache/Cypress for the binary. Key both on the lockfile hash so a Cypress version bump in the lockfile rolls the binary cache at the same time.
What runner size does a headless Chrome Cypress job need?
warp-ubuntu-latest-x64-4x at 4 vCPU and 16GB is the practical floor, and 8x at 32GB is right when the job also runs a dev server, a database, or long specs that accumulate DOM snapshots. The 2x size at 8GB runs out of memory on real suites.
How do I split Cypress specs across parallel jobs without a record key?
Sort the spec files deterministically and give each matrix job every Nth file through the --spec flag. Splitting by file count is coarse, so pin the slowest specs to their own shard and put that shard on a larger warp- label.
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.