GitLab CI Cache Misses: A Diagnostic Guide to Finding Why Your Pipeline Rebuilds Everything
Your GitLab pipeline has a cache block but reinstalls everything every run. Learn to read the cache lines in job logs, match symptoms to causes, and fix keys, paths, and runner storage.
16 Jul 2025, 14:04 UTC

Your pipeline runs, the cache block is in .gitlab-ci.yml, and yet every job downloads dependencies from scratch. The job log shows nothing about cache restore, or it shows "not found" on every run. This guide walks through the conditions that produce cache misses in GitLab CI/CD, how to read the evidence in job logs, and the fixes matched to each cause. It assumes GitLab 15.x or later with gitlab-runner 15+; cache behavior is stable across these versions, but check your runner version with gitlab-runner --version on the runner host before assuming the behavior described here.
What a cache miss actually looks like
GitLab reports cache activity in the job log, not in the UI. Scroll to the "Restoring cache" section near the top of a job log. You will see one of three outcomes:
- "Checking cache for <key>..." followed by "Successfully extracted cache" — cache hit, not your problem.
- "Checking cache for <key>... No URL provided, cache will not be downloaded" — the runner has no distributed cache configured, so nothing can be shared between runners or machines.
- "Checking cache for <key>... not found" or the key shown is different from what you expected — a key mismatch or the cache was never uploaded.
The "Creating cache" section at the end of the job is the other half of the story. If a job fails before that section runs, the cache is never uploaded, and the next job will always miss.
Cause and diagnostic table
| Symptom in job log | Likely cause | Where to confirm |
|---|---|---|
| "No URL provided, cache will not be downloaded" | Runner has no distributed (S3/GCS) cache configured | config.toml on the runner host, [runners.cache] section |
| Cache key in log differs between jobs that should share it | Key includes a variable that changes per job or per branch | Compare the "Checking cache for" line across jobs |
| Key matches but always "not found" | Uploading job fails, or cache:policy is pull-only everywhere | Check job exit status and each job's cache:policy |
| Cache restores but dependencies reinstall anyway | cache:paths doesn't cover the real dependency directory | Compare paths against where the package manager actually writes |
| Cache works on one runner, misses on another | Local (non-distributed) cache on a single machine | Runner tags and config.toml cache type |
Ordered checks
- Read the job log first. Find the "Checking cache for <key>" line in two jobs that should share a cache. If the keys differ, stop here — the problem is key construction, not storage.
- Check the runner's cache backend. On the runner host (requires shell access and read permission on the runner config), inspect
/etc/gitlab-runner/config.toml. A[runners.cache]section withType = "s3"(or gcs/azure) and aShared = trueflag means caches are shared across runners. If the section is absent, the runner uses a local directory, and caches only survive on that one machine. Docker executor runners on autoscaled fleets effectively have no cache without distributed storage. - Verify the upload actually happened. In the "Creating cache" section, look for "Created cache" or an error such as an archive size limit or an S3 permission failure. A job that fails or is canceled before this point uploads nothing.
- Check cache policies. If every job sets
cache:policy: pull, no job ever pushes. You need at least one job with the defaultpull-pushor an explicitpush. - Confirm paths exist. GitLab silently skips cache paths that don't exist at upload time. If you cache
node_modulesbut install into a different directory, the upload succeeds with nothing useful in it.
Fixes tied to findings
Unstable cache keys
A common mistake is embedding $CI_COMMIT_REF_SLUG or $CI_JOB_NAME in the key when jobs should share. Each branch then gets its own cache, and first pipelines on new branches always miss. For dependency caches, key off the lockfile instead — GitLab hashes up to two files:
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
This gives you a stable key that only changes when dependencies change. If you want new branches to start from the default branch's cache rather than nothing, add fallback_keys (or the older fallback_key) pointing at a static key your default-branch job maintains.
No distributed cache
Configure S3-compatible storage in the runner's config.toml under [runners.cache] and [runners.cache.s3]. This requires runner admin access and a bucket with credentials. After editing, restart the runner service. Risk: a misconfigured bucket policy causes silent upload failures, so run one test pipeline and confirm "Created cache" appears before rolling out.
Wrong paths
Point cache:paths at the directory the tool actually populates — for example .npm for the npm cache versus node_modules for installed packages. Note that cache paths must live inside the project directory; absolute paths outside $CI_PROJECT_DIR are ignored.
Limitations and verification
Caches are not guaranteed storage: GitLab's own shared runners evict caches, and self-hosted object storage may have lifecycle rules. Treat cache as an optimization, never as a build input. To verify a fix, re-run the pipeline twice on the same branch: the second run's first job should log "Successfully extracted cache" with the expected key, and dependency install time should drop noticeably. If the key matches and restore still fails, check object-storage credentials and bucket lifecycle policies with your runner administrator.
When to escalate
Escalate to whoever owns the runner fleet when: the log shows "No URL provided" on shared runners, uploads fail with storage errors you can't fix from pipeline config, or cache behavior differs between runners with identical tags. Bring the job URLs and the exact "Checking cache for" lines — they pin down whether the failure is keying, storage, or policy.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.