GitLab CI artifacts don’t just vanish; they stick around until a policy explicitly tells them to leave.

Let’s see what this looks like in practice. Imagine a pipeline that builds a Docker image and then uploads it as an artifact.

build_docker_image:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t my-image:latest .
    - docker save my-image:latest > my-image.tar
  artifacts:
    paths:
      - my-image.tar
    expire_in: 1 week

Here, expire_in: 1 week means that my-image.tar will be automatically deleted one week after the pipeline job finishes. This is the most common way to manage artifact lifecycles directly within your .gitlab-ci.yml.

But what if you have hundreds of jobs, each generating artifacts? Managing expiration on a per-job basis becomes tedious. This is where project-level artifact expiration policies shine. You can configure these in your GitLab project’s Settings > CI/CD > Artifacts expiration.

Here, you can define default expiration rules that apply to all jobs in the project, or create specific rules based on job names, branch names, or tags. For example, you could set a default expiration of 30 days for all artifacts, but have artifacts from a specific staging branch expire in just 7 days.

The system evaluates these policies in a specific order. When a job completes, GitLab checks for an expire_in or expire_at directive within that job’s artifact definition in the .gitlab-ci.yml. If found, that takes precedence. If not, it then looks at the project-level artifact expiration settings. If a job matches multiple project-level rules, the most restrictive rule (the one that causes the artifact to expire sooner) is applied.

Consider a scenario where you want to keep artifacts from your main branch indefinitely, but clean up everything else after 90 days.

On the project settings page, you’d configure:

  • A default expiration of 90 days for all artifacts.
  • A specific rule for branches matching main with an expiration of Never expires.

This ensures that your production-related artifacts are preserved, while temporary build outputs are automatically pruned.

You can also use expire_at for specific timestamps, which is useful for time-bound promotions or releases. For instance, you might want an artifact to be available only until a specific date and time.

deploy_release_candidate:
  stage: deploy
  script:
    - echo "Deploying release candidate..."
  artifacts:
    paths:
      - release_notes.md
    expire_at: "2024-12-31T23:59:59Z" # Expires at the end of 2024 UTC

This expire_at is a hard deadline. Once that timestamp passes, the artifact is marked for deletion, regardless of any other policies.

The actual deletion process is handled by a background worker in GitLab. It periodically scans for artifacts whose expiration time has passed and removes them. This isn’t instantaneous; there can be a delay between an artifact expiring and it being physically removed from storage.

The most surprising thing is that even if you set an artifact to "Never expires," GitLab still tracks it. It just doesn’t schedule it for deletion. This metadata is crucial for the system to know not to touch it, even when other cleanup jobs run. If you later decide to enforce an expiration, you can simply update the policy.

If you try to download an artifact that has expired, you will receive a 404 Not Found error.

Want structured learning?

Take the full Gitlab-ci course →