A slow pipeline usually repeats the same expensive setup work again and again. It downloads the same dependencies, rebuilds the same package indexes, and recreates the same local caches on every run. GitLab CI/CD caching exists to reduce that waste.
When used well, cache can cut minutes from pipelines. When used badly, it causes confusion because people expect it to behave like a guaranteed file handoff. The key is understanding what cache is actually for.
A cache in GitLab CI/CD stores files so future jobs or future pipeline runs can reuse them instead of recreating them from scratch. The goal is speed.
Common cached content includes:
Cache is best for inputs that are expensive to fetch but safe to rebuild.
People often confuse cache and artifacts because both involve saved files. The difference is important.
| Feature | Purpose | Typical Content | Lifetime | Reliability Goal |
|---|---|---|---|---|
| Artifacts | Preserve outputs for later jobs or download | Build outputs, reports, release bundles | Usually tied to a pipeline | Correctness |
| Cache | Reuse reusable files to save time | Dependencies, package caches, temporary build inputs | Across jobs and pipelines | Performance |
A short rule helps:
Never build a pipeline that depends on cache for correctness. Cache can be missed, replaced, invalidated, or rebuilt.
cache: paths:The basic form is straightforward.
test:
stage: test
image: node:20
cache:
paths:
- node_modules/
script:
- npm ci
- npm testThis tells GitLab to store and restore the node_modules/ directory as a cache.
pathsCache paths should be directories or files that:
Good examples:
.npm/.cache/pip.m2/repository/.gradle/Potentially risky examples:
Those are usually better handled as artifacts.
cache: key:A cache path alone is not enough. GitLab also needs a key to know which cache bucket to use.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- node_modules/This creates a branch-based cache. Each branch gets its own cache because $CI_COMMIT_REF_SLUG expands to a branch-safe string.
Without a thoughtful key, unrelated pipelines may overwrite each other or fail to reuse useful data. A good key balances two goals:
A very common strategy is one cache per branch.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/Benefits:
Tradeoff:
Another smart approach is to base the cache on dependency lock files.
cache:
key:
files:
- package-lock.json
paths:
- node_modules/Now GitLab refreshes the cache when package-lock.json changes. This is often better than a pure branch key because it ties reuse to dependency state rather than branch name alone.
You can also combine ideas conceptually: branch context for isolation and file-based invalidation for freshness.
$CI_COMMIT_REF_SLUG$CI_COMMIT_REF_SLUG is one of the most useful built-in variables for caching. It produces a sanitized version of the branch or tag name that is safe for use in cache keys.
cache:
key: npm-$CI_COMMIT_REF_SLUG
paths:
- .npm/This pattern is simple and readable. It is especially helpful in tutorials and smaller projects because you can immediately understand what the cache is keyed on.
GitLab cache behavior can be adjusted with policy:.
pull-pushThe default behavior is effectively pull then push.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
policy: pull-pushThis is the general-purpose default and works well for many projects.
pullUse pull when a job should only restore cache, not update it.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
policy: pullThis can speed up read-heavy jobs and prevent several jobs from all trying to upload nearly identical caches.
A common pattern is:
pull-pushpullpushUse push when a job should create or refresh a cache but does not need to restore one first.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
policy: pushThis is less common, but it can be useful for dedicated warm-up jobs.
when:GitLab also lets you control when a cache is saved.
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
when: on_successTypical values are:
on_successon_failurealwayson_successThis is the safest common choice. It avoids publishing a cache created by a broken setup.
on_failureSometimes failure-time cache updates help with debugging or with partially completed package downloads, but use this carefully because failed jobs may leave inconsistent state.
alwaysThis saves cache regardless of job outcome. It can help in some long setup flows, but it increases the risk of uploading an incomplete or undesirable cache.
For most beginner pipelines, on_success is the right mental default.
node_modulesNode.js pipelines often spend a lot of time downloading packages. Caching can help.
build:
stage: build
image: node:20
cache:
key:
files:
- package-lock.json
paths:
- .npm/
script:
- npm ci --cache .npm --prefer-offline
- npm run buildThis pattern is often better than caching node_modules/ directly because npm can manage its own cache more predictably.
node_modules/test:
stage: test
image: node:20
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci
- npm testThis can work, but be aware that some teams prefer caching package manager caches rather than installed dependency directories because installed directories can be bulkier and sometimes less stable across environments.
Python projects often cache pip downloads or even a local virtual environment.
test-python:
stage: test
image: python:3.12
cache:
key:
files:
- requirements.txt
paths:
- .cache/pip/
script:
- pip install --cache-dir .cache/pip -r requirements.txt
- pytestThis is a very common and safe optimization.
lint-python:
stage: test
image: python:3.12
cache:
key: venv-$CI_COMMIT_REF_SLUG
paths:
- .venv/
script:
- python -m venv .venv
- . .venv/bin/activate
- pip install -r requirements.txt
- flake8 .Caching .venv/ can be effective, but it is more environment-sensitive than caching pip downloads. Use it when you understand the tradeoffs.
Java builds often benefit greatly from caching Maven dependencies.
maven-build:
stage: build
image: maven:3.9-eclipse-temurin-21
cache:
key:
files:
- pom.xml
paths:
- .m2/repository/
script:
- mvn -Dmaven.repo.local=.m2/repository packageThe Maven local repository can be expensive to populate, so caching it often provides major wins.
Gradle projects have their own cache directories.
gradle-test:
stage: test
image: gradle:8.9-jdk21
cache:
key:
files:
- build.gradle
- settings.gradle
paths:
- .gradle/
script:
- gradle testAgain, the principle is the same: cache dependencies and reusable tool data, not unique release outputs.
A new branch often has no cache yet. GitLab supports fallback keys so a job can try other caches if the primary one is missing.
cache:
key: cache-$CI_COMMIT_REF_SLUG
fallback_keys:
- cache-$CI_DEFAULT_BRANCH
- cache-default
paths:
- .npm/This means:
Fallback keys are especially useful in feature branch workflows because the first pipeline on a new branch can still benefit from dependencies cached on main.
The hardest part of caching is not enabling it. The hard part is knowing when old cache content should stop being reused.
Use package lock files in key.files. When dependencies change, the cache changes too.
This is often the best strategy for Node, Python, and Java ecosystems.
Use $CI_COMMIT_REF_SLUG so each branch has its own cache.
Good when branches diverge heavily or when you want to avoid cross-branch interference.
Manually bump the key when you want to invalidate everything.
cache:
key: npm-v2-$CI_COMMIT_REF_SLUG
paths:
- .npm/If the cache becomes corrupted or the structure changes, moving from v1 to v2 forces a fresh start.
Use branch cache first, then fall back to a stable default branch cache.
This gives you both speed and reasonable freshness.
| Question | Use Artifacts | Use Cache |
|---|---|---|
| Do later jobs need this exact build output? | Yes | No |
| Is the main goal correctness and reproducibility? | Yes | No |
| Is the main goal speed for future runs? | No | Yes |
| Can the files be safely recreated if missing? | Maybe not | Yes |
| Typical examples | dist/, test reports, release bundles | dependency stores, package manager caches |
If you remember only one thing, remember this table.
Caching giant directories blindly is rarely optimal. Start with the package manager cache or dependency store that creates the largest repeated cost.
A pipeline must still succeed when the cache is cold or missing. Cache is an optimization, not a dependency contract.
If unrelated branches or dependency versions all use the same key, weird behavior can follow. Use keys with intention.
Caching should improve job duration noticeably. If it adds upload overhead without saving time, refine it.
Using cache to pass build outputs That is what artifacts are for.
Using one generic cache key forever This causes stale or conflicting content.
Caching too much Uploading huge caches can outweigh the benefit.
Saving cache on failed jobs without a reason This can publish broken cache state.
Assuming every runner shares cache the same way Cache behavior can depend on your runner and storage configuration, so treat it as helpful but not magical.
GitLab cache is one of the simplest ways to make pipelines faster. The winning formula is usually: cache dependency-related directories, choose a thoughtful key, use fallback keys for branch workflows, and remember that cache is about performance rather than correctness. Once you apply that mindset, your pipelines become both faster and easier to maintain.
<MCQExercise exercise={{ id: 'gitlab-cache-q1', title: 'Question 1: Cache vs Artifacts', description: 'When should you prefer cache over artifacts?', options: [ { id: 'a', text: 'When you need to preserve the exact release bundle that will be deployed' }, { id: 'b', text: 'When you want to reuse dependency files to make future jobs or pipelines faster' }, { id: 'c', text: 'When you need GitLab to display a JUnit report in the merge request UI' }, { id: 'd', text: 'When you want to define the order of pipeline stages' }, ], correctId: 'b', explanation: 'Cache is designed for reusable files that improve performance, such as package manager caches and dependency directories. Artifacts are better for exact outputs and reports that matter for correctness.', wrongExplanations: { a: 'Exact release bundles should usually be stored as artifacts, not cache.', c: 'JUnit and other structured reports are handled with artifacts reports, not cache.', d: 'Stage order is defined with the stages keyword, not with cache.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-cache-q2', title: 'Question 2: Cache Keys', description: 'What is the advantage of using package-lock.json or another lock file in cache: key: files:?', options: [ { id: 'a', text: 'It makes the job run only on tagged releases' }, { id: 'b', text: 'It invalidates and refreshes the cache when dependencies change' }, { id: 'c', text: 'It converts cache into artifacts automatically' }, { id: 'd', text: 'It hides the cache from other jobs in the same pipeline forever' }, ], correctId: 'b', explanation: 'A file-based key ties the cache identity to important files such as lock files. When dependencies change, the cache key changes too, helping GitLab create or fetch a fresh cache.', wrongExplanations: { a: 'Lock-file based keys are about cache invalidation, not release scheduling.', c: 'Cache and artifacts remain different features even if keys are file-based.', d: 'Jobs can still use matching caches. The file key simply decides when a cache should be considered the same or different.', }, }} />
<MCQExercise exercise={{ id: 'gitlab-cache-q3', title: 'Question 3: Fallback Keys', description: 'Why are fallback_keys useful for feature branches?', options: [ { id: 'a', text: 'They let a new branch reuse a default branch cache when its own cache does not exist yet' }, { id: 'b', text: 'They disable caching automatically after the first job run' }, { id: 'c', text: 'They force every job to upload a brand new cache even when one already exists' }, { id: 'd', text: 'They turn a cached directory into a manual deployment package' }, ], correctId: 'a', explanation: 'Fallback keys create a search path for caches. A feature branch can first try its own branch cache, then fall back to a main or shared cache, which makes first-run pipelines faster.', wrongExplanations: { b: 'Fallback keys improve cache lookup behavior. They do not disable caching.', c: 'They are meant to increase reuse, not force redundant uploads.', d: 'Deployment packages belong in artifacts, not in cache fallback behavior.', }, }} />
Most often the key does not match. A cache is only restored when a job's key resolves to exactly the same value that stored it, so a key built from a lockfile hash changes the moment that file changes, which is intended, but surprises people. Check the job log, which states whether it found a cache and under which key. The other common causes are caching a path outside the project directory, which is not supported, and jobs running on different runners where the cache is stored locally rather than in shared object storage.
Key the cache on the lockfile so it invalidates when dependencies change, and prefer caching the package manager's download directory over node_modules itself. Restoring a download cache and running npm ci is more reliable than restoring a partially-populated node_modules, particularly across different Node versions or base images. Add a fallback_keys entry pointing at a branch-level key so a new lockfile still gets a partial warm start instead of downloading everything from scratch.
pull-push is the default and means the job restores the cache at the start and uploads it at the end. pull restores without uploading, which is right for jobs that only consume dependencies and would otherwise waste time re-uploading an unchanged cache. push uploads without restoring, useful for a dedicated warming job that builds the cache once at the start of a pipeline. Setting pull on the many jobs that only read is one of the easiest pipeline speed-ups available.
No. Build output belongs in artifacts. Cache has no delivery guarantee, so a job that depends on cached build output will work most of the time and then fail confusingly when the cache is missing or stale, typically on a new branch, or after a runner change. Use cache strictly for things that can be regenerated safely from scratch, and artifacts for anything a later job needs to be certain about.
Because uploading and downloading a large cache can cost more than regenerating its contents. A multi-gigabyte cache over a slow link easily exceeds the time saved, especially on a fast dependency install. Cache selectively rather than broadly, a package manager's download directory rather than an entire workspace, and measure the job duration before and after. Setting policy: pull on jobs that do not change the cache removes half the transfer cost immediately.
Use GitLab CI/CD variables safely: predefined and custom variables, masked and protected values, group-level and scoped variables, and precedence rules.
Learn what a GitLab Runner is and how it runs jobs: shared versus specific runners, executors, installing and registering on Ubuntu, and a Docker executor.
Control exactly when GitLab CI jobs run with rules:if, rules:changes and rules:exists, plus workflow rules, merge request pipelines and manual jobs.