Marcio Cunha

Slow pipeline in GitLab CI? How to use cache to speed up

Learn how to persist dependencies and artifacts between pipeline runs in GitLab CI to drastically reduce developer wait times.

Marcio Cunha•5 min
Also available in:PortuguêsEspañol
Summary
  • Caching stores temporary files and dependencies between runs to avoid repetitive downloads on the same runner.
  • The cache key determines when saved files should be discarded or reused in new builds.
  • Heavy dependencies like node_modules or Maven packages achieve high speed when stored correctly.
  • Incorrect absolute or relative paths prevent GitLab from finding previously saved files.
  • Conscious cache usage reduces compute minute consumption on continuous integration servers.

Why does your continuous integration pipeline take so long to finish?

Anyone working with software development has experienced the frustration of pushing code to a repository and waiting precious minutes just for the server to download the exact same libraries from scratch. In automation platforms like GitLab CI, continuous integration runs repetitive tasks with every pushed change. When there is no memorization mechanism, each install command downloads gigabytes of packages from the internet. In practice, this means wasted operational time and bottlenecks that frustrate the entire engineering team.

To understand the severity of the problem, imagine going to the grocery store and throwing away your entire shopping cart upon arriving home, only to walk back through the store aisles from scratch for your next meal. That is precisely what happens when a build server discards its dependency directory after finishing a task. To avoid this rework, modern tools allow storing these files in a dedicated space, a feature known as caching. The cache acts as a fast drawer where we keep heavy packages for immediate reuse in upcoming runs. To deepen resource management in automated environments, it is also worth checking out our article on how to use sccache to speed up slow builds.

The concept of caching in GitLab CI versus build artifacts

Many people confuse the concept of caching with build artifacts, although they serve completely different purposes in automation architecture. Caching is used exclusively to store external dependencies that do not change frequently, such as Node.js modules or compiled language packages. It is volatile, meaning the server can delete it at any time if disk space runs low. Build artifacts, on the other hand, are final build results, such as binaries or test reports, which must be preserved for download or use in subsequent pipeline stages.

When we configure the cache correctly in the GitLab configuration file, the system compresses the specified directories and uploads them to centralized storage at the end of the job. In the next run, before even running the main code, the server downloads this compressed archive and extracts it into the project folder. This process turns a multi-minute internet download into a simple internal extraction in a few seconds. It is a massive efficiency gain aligned directly with infrastructure management best practices, similar to the strategies we discuss when analyzing why pods get OOM Killed in Kubernetes and how to adjust limits in dense production environments.

How to structure the cache key to prevent corrupted data

The biggest challenge when configuring cache in GitLab CI is not just making it work, but ensuring it does not serve outdated or corrupted files. The mechanism uses text keys to identify which files belong to which context. If the key is too generic, different branches of the application will overwrite the same cache, causing mysterious library version conflicts. The best approach is to link the cache key to the dependency control file, such as package-lock.json, yarn.lock, or pom.xml.

In practice, this means GitLab will generate a unique identifier based on the content of that dependency file. If no new package was added or removed in the commit, the control file remains identical and the cache is reused with total safety. If a developer alters dependencies, the key changes automatically, forcing the system to download new packages and create an updated cache for subsequent runs. This strategy eliminates phantom bugs caused by old packages stuck on the server and ensures total predictability in deliverables.

Practical configuration example in the gitlab-ci.yml file

Implementing this optimization in practice requires only a few well-structured lines in the pipeline configuration file. Below, we present a functional template focused on a Node.js application that uses the npm package manager to install dependencies quickly without wasting bandwidth.

stages:  - build  - testcache_job:  stage: build  image: node:18-alpine  cache:    key:      files:        - package-lock.json    paths:      - node_modules/  script:  - npm ci  - npm run build

In the code snippet above, the cache directive specifies that the node_modules folder will be saved and restored automatically based on changes to the package-lock.json file. The npm ci command performs a clean, ultra-fast installation using strictly the versions locked in the lockfile. If you notice runner disk space getting constrained by accumulated temporary files, it is worth implementing regular cleanup routines, similar to what we explain in the guide on how to clean Docker cache on Linux and free up space.

Common pitfalls and mistakes when using cache in distributed environments

Despite being powerful, incorrect cache usage can generate unexpected behaviors that stall team productivity. A classic mistake is trying to save absolute paths outside the project working directory, which is blocked for security reasons by GitLab CI. Another common issue occurs when multiple runners operate on different servers without shared storage configured, causing caching to work intermittently depending on which machine picked up the job.

To overcome these limitations in scalable environments, it is essential to ensure the runner uses a compatible storage driver, such as S3 or MinIO, centralizing the temporary file repository. Furthermore, avoid placing gigantic and mutable folders in the cache, such as log directories or local test databases, because the time spent compressing and uploading this data to the cloud can exceed the time saved during task execution.

Final thoughts on speed and operational efficiency

Accelerating continuous integration pipelines is not merely a matter of technical vanity, but a decisive factor for the mental health and agility of any development team. When feedback loops shrink from twenty minutes to two minutes, engineers gain autonomy to test hypotheses, fix bugs, and deliver value to end-users with much higher confidence. Mastering cache usage in GitLab CI turns testing infrastructure into a fast and predictable ally, eliminating invisible barriers that slow down innovation pace in modern companies.