Why does caching node_modules not stop Cypress downloading its binary in CI?
answer
- Two artefacts land in two different places
- Only one of them is inside the project
- A no-op install runs no postinstall
- The global cache is what needs persisting
- The variable is read at launch too
basics
~20 sThe Cypress binary is not in node_modules. Only the small npm module is; the application binary is downloaded by a postinstall script into a global cache outside the project, so that directory is what a pipeline has to persist.
solid answer
~40 sCypress installs in two places. `node_modules` gets the small npm module — CLI, types and launcher — while the actual application binary is fetched by a `postinstall` script into a global cache outside the project, `~/.cache/Cypress` on Linux. Restoring `node_modules` restores the launcher and none of what it launches. It is often worse than that: a restored `node_modules` makes the install a no-op, so `postinstall` never runs and the binary is never downloaded at all, failing later as a missing application. The fix is to persist the binary cache directory, or point `CYPRESS_CACHE_FOLDER` at a directory the pipeline already persists — and to make sure that variable is set for the step that runs the suite, not only the install step.
code
bash · 4 linesexport CYPRESS_CACHE_FOLDER="$PWD/.cache/Cypress"
npm ci
npx cypress cache path
npx cypress cache list --sizego deeper
Recall that installing Cypress downloads a separate application binary, and that it is stored outside the project in a global cache rather than beside your other dependencies.
Explain the postinstall model and name the default cache locations. Be able to say why the package manager's own cache is the right thing to persist rather than the installed dependency folder.
An interviewer expects a diagnosis: prove where the binary was looked for, explain why a restored dependency folder can suppress the download entirely, and know that the cache variable is read at launch as well as at install.
Own the trade-off between a prebuilt image that carries the runner and a cached download that each job restores, including who pays for cache storage, how versions are keyed, and how the standard is kept consistent across repositories.
## Two artefacts, two locations The reason the cache miss surprises people is that Cypress installs in two places, and only one of them is inside your project: - The **npm module** (`cypress`) lands in `node_modules`. It is small: a CLI, types and a launcher. - The **Cypress application binary** is downloaded by a `postinstall` script into a **global cache outside `node_modules`**, so a single download can be shared by every project on the machine. The cache is machine-wide, and where it lives depends on the platform the agent runs: | Platform | Default binary cache | | --- | --- | | Linux | `~/.cache/Cypress` | | macOS | `~/Library/Caches/Cypress` | | Windows | `/AppData/Local/Cypress/Cache` | Restoring `node_modules` therefore restores the launcher and nothing that launcher needs. Worse, it often makes things *actively* worse than a cold job. ## Why restoring node_modules can prevent the download entirely If `node_modules` is already present and looks satisfied, the install becomes close to a no-op — and a no-op install runs no `postinstall` script. The binary is never fetched at all, and the failure shows up later, at launch, as a missing Cypress application rather than as an install error. This is why the guidance is to **not cache `node_modules` across builds** and to cache the package manager's own cache directory instead (`~/.npm` for npm, `~/.cache/yarn` for Yarn). Those tools track installed versions, validate integrity and re-download only what changed; a restored `node_modules` bypasses all of it. ## What to persist instead 1. **Persist the binary cache.** Restore `~/.cache` (or just the Cypress cache inside it) after the dependency install step. On Linux, caching `~/.cache` with Yarn conveniently covers both the Yarn cache and the Cypress binary. 2. **Or relocate the cache with `CYPRESS_CACHE_FOLDER`.** Point it at a directory your pipeline already persists — often somewhere inside the workspace — and the multi-minute download becomes a cache hit. It is also the escape hatch when the default location is not writable on a locked-down agent. 3. **Verify where Cypress is actually looking.** `cypress cache path` prints the resolved cache folder, and `cypress cache list` prints the versions in it with when each was last used. Two lines in a job settle the argument about whether the restore worked. ## The mistake that makes CYPRESS_CACHE_FOLDER look broken `CYPRESS_CACHE_FOLDER` is read **every time Cypress is launched**, not only when it is installed. A job that exports it for the install step and then runs the suite in a step that does not see it will look in the default location, find nothing, and fail or re-download. The directory also has to exist at launch time. Export it once for the whole job — or commit it as `cypress_cache_folder` in an `.npmrc` — rather than inlining it on one command. A `~` in the value is expanded to the user's home directory, so a value like `~/.cache/Cypress` is safe to pass as a plain string from a configuration file. ## Two more failure modes on restored caches - **Snowballing.** A restore key that is too loose lets the cache accumulate a binary for every Cypress version the repo has ever used. Each is hundreds of megabytes, and in a storefront monorepo where several packages upgrade at different times it grows fast. Keying the cache on the resolved Cypress version, and pruning, is the answer. - **Verification on a read-only restore.** A launch runs `cypress verify`, a smoke test of the installed binary, and records the result next to the binary so later launches can skip it. If the restored cache is not writable, verification cannot record anything and the launch can fail with a permission error. `CYPRESS_SKIP_VERIFY=true` suppresses the step; `CYPRESS_VERIFY_TIMEOUT` (default `30000` ms) raises the 30-second budget on a heavily loaded agent. ## Reading the evidence When a job that "should" be cached is still slow, check these in order: - Did the dependency install actually run, or was it skipped because `node_modules` came back from a cache? - Does `cypress cache path` print the directory you persisted, in the step that runs the suite? - Does `cypress cache list` show the version the project resolves to, or a different one? - Is the run printing a warning that the binary version does not match the expected package version? That means a binary was restored from an older run and the package moved on. If the image itself already carries Cypress — the `cypress/included` variant installs it globally — none of this applies to that copy, but a package that also declares `cypress` as a dependency will still install and cache its own binary alongside it.
- A job exports CYPRESS_CACHE_FOLDER for the install step only. What goes wrong?Cypress reads that variable every time it is launched, not only when it installs. The suite step looks in the default location instead, finds no binary there and either fails or downloads one again — so the job pays the download it was trying to avoid. Export the variable once for the whole job, and make sure the directory exists at launch time.
- A restored Cypress cache is read-only and the run fails during verification. What are your options?A launch runs `cypress verify`, a smoke test whose result is recorded next to the binary so later launches can skip it; a read-only cache cannot record it. Either restore the cache to a writable location, or set `CYPRESS_SKIP_VERIFY=true` to suppress the step. If verification is merely slow rather than failing, raise `CYPRESS_VERIFY_TIMEOUT` above its 30000 ms default instead.
saying these in an interview costs you the question
- Believes the Cypress binary lives inside node_modules
- Caches node_modules and expects the binary to come with it
- Sets CYPRESS_CACHE_FOLDER only on the install step
- Never checks cypress cache path to confirm the location
- Ignores the binary-versus-package version mismatch warning