The consuming job asked for an artifact that is not there. Four causes, in the order they occur.
1. The name does not match
The name is a literal string. The most common failure is an expression producing a different one per job:
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }} # build-ubuntu-latest, build-macos-latest
path: dist/
- uses: actions/download-artifact@v4
with:
name: build # matches nothing
Either name them consistently, or download everything:
- uses: actions/download-artifact@v4
with:
pattern: build-*
merge-multiple: true
path: dist/
pattern and merge-multiple were added in v4 precisely for the matrix case.
2. v4 changed the rules
This broke a great many pipelines and is worth knowing in full:
| v3 | v4 | |
|---|---|---|
| Same name from several jobs | Merged into one artifact | Conflict error |
| Cross-workflow download | Possible | Needs run-id and a token |
| Immutability | Mutable | Immutable once uploaded |
| Speed | Slower | Much faster |
Error: Conflict: an artifact with this name already exists on the workflow run
In v3 every matrix leg uploading build merged silently. In v4 the second one fails. Give each a unique name and merge on download:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}
path: dist/
publish:
needs: build
steps:
- uses: actions/download-artifact@v4
with:
pattern: build-*
merge-multiple: true
path: dist/
Do not mix versions: a v4 download cannot see a v3 upload, and the reverse also fails.
3. The upload silently matched nothing
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/
If dist/ does not exist, this warns and succeeds. The job is green and no artifact exists, and the failure surfaces later in a different job, which is a confusing place to debug it.
if-no-files-found: error
Worth setting on every upload. It converts a silent miss into a failure at the point where the problem actually is.
Check the path too: it is relative to the workspace, and a build that writes to packages/app/dist is not matched by dist/.
4. Job ordering
publish:
needs: build # without this, they run in parallel
Without needs, jobs run concurrently and the consumer may start before the producer finishes. That produces an intermittent failure that passes on a slow build and fails on a fast one.
In GitLab, stage order handles it:
stages: [build, deploy]
build:
stage: build
artifacts:
paths: [dist/]
expire_in: 1 week
deploy:
stage: deploy
dependencies: [build] # only fetch this job's artifacts
GitLab passes artifacts from all earlier stages by default. dependencies narrows it, and an empty list disables the download entirely:
dependencies: []
That is worth setting on jobs that need no artifacts, since a large artifact downloaded into every job costs real time.
Expiry
GitLab artifacts expire, and the default is often shorter than people assume:
artifacts:
paths: [dist/]
expire_in: 1 week
ERROR: Downloading artifacts from coordinator... not found responseStatus=404
A 404 on a job from last month is usually expiry rather than a fault. expire_in: never keeps them, at a storage cost. Use keep_latest_artifact in project settings to retain the most recent per ref.
GitHub Actions artifacts default to 90 days, configurable per upload:
retention-days: 7
Shorter retention is worth setting for large build outputs, since artifact storage counts against your account quota.
Across workflows
In v4 this needs explicit parameters:
- uses: actions/download-artifact@v4
with:
name: build
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
Without run-id and a token, the action only looks within the current run. This is the standard pattern for a workflow_run workflow consuming output from the workflow that triggered it.
Artifacts are not a cache
Worth stating because it causes both errors and cost:
| Artifacts | Cache | |
|---|---|---|
| Purpose | Pass output between jobs, keep build results | Speed up repeated work |
| Missing | An error you must handle | A miss, the job continues |
| Scope | One workflow run | Across runs, keyed |
Using artifacts for node_modules is slow and expensive. Use the cache action, which is designed to tolerate a miss.
A checklist
- Compare upload and download names character for character.
- Matrix jobs → unique names, then
patternplusmerge-multipleon download. - Do not mix
upload-artifactv3 withdownload-artifactv4. - Set
if-no-files-found: errorso an empty upload fails loudly. - Confirm the path is relative to the workspace and actually contains files.
- GitHub →
needs:. GitLab → a laterstage. - GitLab →
dependencies: []on jobs that need nothing, to save time. - 404 on an older run → expiry. Check
expire_inandretention-days.
Frequently Asked Questions
Why did upload-artifact v4 break my matrix build?
Because v4 removed the implicit merging that v3 did. In v3, several jobs uploading under the same name were combined into one artifact; in v4 names must be unique within a run and the second upload fails with a conflict. Give each matrix leg a distinct name such as build-${{ matrix.os }}, then download with pattern: build-* and merge-multiple: true, which reassembles them into one directory. The versions are also incompatible in both directions, so upload and download must match.
Why does my upload succeed but the download find nothing?
Because upload-artifact only warns when its path matches no files; the step and the job both go green having uploaded nothing. The failure then surfaces in a different job, which is a misleading place to start debugging. Set if-no-files-found: error on every upload so the failure happens where the problem is. Also verify the path: it resolves relative to the workspace, so a monorepo writing to packages/app/dist is not matched by dist/.
Why is the failure intermittent?
Almost certainly missing job ordering. Without needs: in GitHub Actions, jobs run in parallel, so whether the consumer finds the artifact depends on which job happens to finish first. A slow build masks it and a fast one exposes it. In GitLab, jobs within a stage run in parallel and only later stages see earlier artifacts, so a consumer placed in the same stage as its producer has the same race.
What is the difference between artifacts and cache?
Artifacts pass build output between jobs and are meant to be retrievable afterwards; a missing artifact is an error your pipeline has to handle. Cache exists to speed up repeated work and is keyed, shared across runs, and safe to miss, since a cache miss simply means the job does more work. Using artifacts for dependency directories such as node_modules is slower and consumes storage quota, and using cache to pass build output between jobs is unreliable because a miss is silent.
Why do older artifacts return 404?
They expired. GitLab's expire_in is commonly set to days or weeks and defaults to a limited period, and GitHub Actions retains artifacts for 90 days by default. A download from a job run last month is therefore likely to be genuine expiry rather than a fault. Set expire_in: never or a longer retention-days for artifacts you need to keep, bearing in mind that storage counts against your account's quota and large build outputs add up quickly.