Controlling GitLab CI Artifact Expiration to Stop Storage Bloat
GitLab CI artifacts are kept forever unless you set artifacts: expire_in. Learn how to configure per-job retention, avoid common mistakes, and verify cleanup actually happens.
29 Apr 2026, 18:41 UTC

The short answer
Every GitLab CI job that produces artifacts keeps them forever unless you say otherwise. Over months, this turns into gigabytes of stale build outputs, test reports, and dependency caches that inflate storage costs and can push self-hosted instances or SaaS projects into quota limits. The fix is a single line in your job definition: artifacts: expire_in. Set it per job, and GitLab deletes the artifacts automatically after the period you choose.
How expiration actually works
When a job finishes, GitLab uploads whatever paths you declared under artifacts: paths to object storage (or local disk on self-hosted instances). Each artifact archive carries an expiration timestamp. A background worker periodically scans for expired artifacts and deletes them — deletion is not instant at the moment of expiry, so expect a lag of minutes to hours depending on instance load.
Two behaviors matter here:
- Latest successful artifacts are kept by default. GitLab protects the artifacts of the most recent successful pipeline per ref, even if their expiry has passed, so that
needsand download links keep working. You can disable this per project under Settings > CI/CD > Artifacts ("Keep artifacts from most recent successful jobs"). expire_inonly applies when artifacts are actually uploaded. If a job has noartifactsblock at all, there is nothing to expire. The setting lives inside theartifactsmap, not at job top level.
A worked configuration
Here is a realistic pipeline with differentiated retention. Build outputs are only needed for downstream jobs in the same pipeline, so they expire fast. Release packages are kept longer for audit purposes:
stages: [build, test, package]
build:
stage: build
script:
- make build
artifacts:
paths:
- dist/
expire_in: 2 hours # only needed by later jobs in this pipeline
test:
stage: test
script:
- make test
artifacts:
when: always
reports:
junit: reports/junit.xml
expire_in: 1 week # useful for debugging flaky tests
package:
stage: package
script:
- make package
artifacts:
paths:
- dist/*.tar.gz
expire_in: 30 days # release candidates, kept for audit
The expire_in value accepts duration strings such as 15m, 2h, 1 day, 1 week, or 30 days. You can also set a default for every job at once:
default:
artifacts:
expire_in: 1 week
Jobs that need different retention override the default in their own artifacts block.
Choosing sensible retention periods
Match retention to how the artifact is consumed:
| Artifact type | Suggested expiry | Reasoning |
|---|---|---|
| Intermediate build output passed to later stages | 1–6 hours | Only needed while the pipeline runs |
| Test reports and coverage data | 1 week | Enough time to debug failures |
| Nightly/QA packages | 7–30 days | Manual testing windows |
| Release artifacts | Keep, or publish to a package registry | CI artifacts are the wrong long-term store |
If you find yourself setting multi-month retention on release binaries, that is a sign the artifact should live in GitLab's Package Registry or an external artifact repository instead. CI artifact storage is optimized for pipeline handoff, not distribution.
Limits and common mistakes
- Putting
expire_inat the wrong level. It must sit inside theartifactsmapping. A top-levelexpire_inkey is invalid and will fail pipeline validation. - Assuming expiry deletes instantly. Deletion happens via a scheduled cleanup worker. Storage dashboards lag behind.
- Forgetting the "keep latest successful" behavior. Teams often think expiry is broken when the newest artifacts survive. Disable the project setting if you want strict expiry, but be aware downstream pipelines referencing those artifacts will break.
- Oversized artifacts. Individual jobs producing artifacts over a few gigabytes slow upload/download and bloat storage quickly. Split outputs or exclude directories like
node_modules(use a cache or exclude patterns instead). - Compliance conflicts. Extending retention beyond your organization's data-retention policy (often 30 days for build data) can violate internal or regulatory rules. Check before setting long expiries.
- Retried jobs. When a job is retried, the new artifact replaces the old one for download purposes, but cleanup of superseded archives is not always immediate; occasionally manual deletion via the UI or API is needed.
Verifying it worked
After a pipeline runs, open the project and go to Build > Jobs, click the job, and look at the artifacts section in the job detail page — it shows the artifact size and its expiration time. For a storage-level view, check the project's usage quotas page (Settings > Usage Quotas on SaaS) to see total artifact storage trend down after cleanup runs. On self-hosted instances, administrators can also inspect artifact storage directly and confirm the housekeeping cron is running. If expired artifacts persist for more than a day, check that the instance's background jobs are healthy before assuming your YAML is wrong.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.