Synthetic Industry

Troubleshooting guide · updated 2026-10-11

CI reinstalls dependencies on every run: find out why the cache never hits

How a CI dependency cache is looked up, five reasons it never serves a later run, a safe two-run comparison, and how the paid fix is accepted.

Start with how the lookup works

A dependency cache is a saved folder with a name. On GitHub Actions a run first looks for a cache whose name equals the key exactly. If there is none, it tries each restore key as a prefix, in the order you listed, and takes the most recently created match. Only an exact match counts as a hit; a prefix match restores something but is reported as a miss. At the end of a job the action saves a new cache under the key, but only if the job succeeded and only when the lookup was a miss. A saved cache is never edited: to change it you save a new one under a new key.

That is why a good key contains something that changes exactly when the dependencies change, such as a hash of the lockfile. A key that never changes restores stale folders; a key that changes every run, for example because it includes a timestamp or run number, can never match.

  • Exact key match is a hit; a restore-key match is a partial restore.
  • The cache is saved at the end of a successful job, under the key, when nothing matched exactly.

Five reasons a cache never serves a later run

Work through them in this order, because each one is cheap to check from two run logs.

First, the key changes on every run. Second, the scope is wrong: a run can restore caches from its own branch and from the default branch, and a pull request run can also restore from its base branch, but not from a sibling branch, and a cache created by a pull request run belongs to that pull request's merge reference, so only re-runs of that same pull request can use it. Third, nothing was ever saved: if the job fails before the end, which a persistently red pipeline does, the save step does not store a cache. Fourth, the entry expired or was evicted: entries not used for seven days are removed, and past the repository allowance of 10 GB the oldest-used entries are removed first, so a repository that runs rarely loses its cache between runs. Fifth, the cached path is not the folder the install command actually reads, so a restore succeeds and the install still downloads everything.

  • Compare the key printed in two runs.
  • Check which branch each run came from.
  • Check whether the job reached its final step.
  • Check the age of the previous run against the seven-day rule.
  • Compare the cached path with the install command's folder.

A safe first investigation

Open two runs of the same job, one that ran first and one that ran after it with no dependency change. Do not re-run anything and do not turn on extra logging. In each run, read the cache step: the key it printed, whether it says it found a cache, and which restore key matched if any. Then read the install step's duration in both runs. If the second run reports a hit and the install is still slow, the path is the likely cause. If it reports a miss and the key is identical, look at scope and age. If the keys differ, find the part of the key that changed.

On GitLab CI the same approach applies with different traps. The cache lives on the runner by default, so a later job finds it only if it lands on the same runner, unless distributed caching is configured. GitLab's own guide calls caching an optimisation that is not guaranteed to work, so a job should still be able to rebuild what it needs.

What fixes it, and what does not

Fixes that match the causes: build the key from a hash of the lockfile, list restore keys from most specific to least specific, point the cache at the folder the package manager reads, and let only push-type triggers on the default branch write the shared cache so that a low-trust trigger cannot fill it. A pipeline that is always red needs its failures fixed before any cache can be saved.

What does not fix it: a bigger runner, a bigger cache allowance, or caching more folders. Those cost money and hide the real cause. Nor does a green re-run: re-running reuses the same commit and does not tell you whether the key matched.

How the paid outcome is accepted

The fixed job covers one pipeline and one cache. It is accepted by evidence you can read yourself: a warm run that reports an exact key hit, a dependency step at or under a duration you agreed before work began on the same runner type, and a run after a lockfile change that misses and then saves a fresh cache. It does not promise a time saving, because network speed and eviction vary. The price is £195, untested, paid after you sign off.

Sources and limits

  • GitHub dependency caching reference Checked 2026-10-11.
    • An exact key match is a hit; restore keys match by prefix in the order listed; the most recent matching cache is restored.
    • A cache saved by a pull request run can only be restored by re-runs of that pull request; entries unused for 7 days are removed and the default repository allowance is 10 GB.
    • On a miss the cache is saved under the key only if the job succeeds; existing cache contents cannot be changed.
  • GitLab caching guide Checked 2026-10-11.
    • Caching is an optimisation that is not guaranteed to work, and a runner-local cache is only found by jobs on the same runner.
    • A cache key can be derived from the contents of chosen files.
  • Existing GitHub Actions single-job repair Checked 2026-10-11.