GitHub Actions upload-artifact 409: Fix “Artifact Already Exists”
actions/upload-artifact can fail with 409 Conflict or an artifact with this name already exists when two uploads in the same workflow run target one artifact name. With the v4 artifact model, an artifact is immutable after creation unless it is explicitly deleted and replaced. The normal fix for a matrix is to make the name unique; overwrite: true is for a sequential replacement, not for combining parallel uploads.
Decide whether the outputs are separate or one bundle
The right fix depends on what the artifact represents:
| Workflow shape | Correct design |
|---|---|
| Each matrix job produces an independent build | Add the matrix value to name |
| Several jobs should become one downloadable archive | Upload unique artifacts, then merge them in a later job |
| A later job should replace an older snapshot | Use overwrite: true after the earlier upload has finished |
| One job has several files | Upload a directory or a deliberate file set in one step |
Do not give every parallel job name: build. The name is part of the artifact identity, so the jobs are racing to create the same object rather than appending files to a shared directory.
Give matrix jobs unique artifact names
The most predictable matrix pattern is to include every value that changes the output:
jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] node: [22, 24] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v5 with: node-version: ${{ matrix.node }} - run: npm ci - run: npm run build - uses: actions/upload-artifact@v4 with: name: build-${{ matrix.os }}-node-${{ matrix.node }} path: dist/Use the values that actually distinguish the files. If the build varies by browser, architecture, feature flag, or package, include that dimension as well. A unique name prevents a conflict, but it also makes the artifact list useful when you need to compare outputs after a failed run.
The same rule applies when the upload uses archive: false. In that mode the uploaded file’s name determines the artifact name, so two jobs can still collide even when a name input appears in the step. Make the source filenames unique or keep the default archive behavior when you need an explicit artifact name.
Merge parallel artifacts after the matrix
If the reader of the workflow needs one artifact containing files from every matrix job, do not make the matrix jobs write to one artifact. Upload separate artifacts first, then add a dependent merge job:
jobs: build: strategy: matrix: platform: [linux, macos, windows] runs-on: ${{ matrix.platform }}-latest steps: - uses: actions/checkout@v5 - run: ./scripts/build-${{ matrix.platform }}.sh - uses: actions/upload-artifact@v4 with: name: package-${{ matrix.platform }} path: out/
merge: needs: build runs-on: ubuntu-latest steps: - uses: actions/upload-artifact/merge@v4 with: name: all-packages pattern: package-*The merge job must wait for the matrix. That ordering turns many immutable artifacts into one new artifact without asking parallel jobs to mutate shared state. Keep the names and pattern narrow enough that an unrelated artifact from another step cannot be pulled into the bundle.
Reserve overwrite for a finished artifact
For a sequential workflow that intentionally publishes a fresh snapshot under the same name, use overwrite: true:
jobs: upload-preview: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: build-output path: preview/ - uses: actions/upload-artifact@v4 with: name: preview path: preview/ overwrite: trueThis replaces the previous artifact and creates a new artifact record. It does not merge files into the existing archive, and it is not a safe way to resolve simultaneous matrix uploads. If two jobs can reach the step at once, use unique names and a later merge instead.
Check the rendered workflow before changing the action
When the error is not obvious, inspect the workflow as a set of jobs rather than changing action versions repeatedly:
- Search every
actions/upload-artifactstep and list its finalnameafter expression expansion. - Check matrix dimensions, reusable-workflow inputs, and job fan-out.
- Look for
archive: false, because the file basename may become the identity. - Confirm that a retry or rerun is not intentionally reusing a name that should be unique per run.
- Decide whether a separate artifact, a merge job, or a sequential replacement matches the consumer.
If the upload then fails because a file is absent, that is a different boundary. Set if-no-files-found: error for required output and use the existing GitHub Actions hidden-files guide when dot-prefixed build files are part of the intended artifact. Do not enable hidden-file uploads merely to hide a path mistake.
The useful distinction is simple: parallel jobs should create different artifact names, a merge job should assemble a single bundle, and overwrite should replace one completed snapshot in a controlled sequence.
FAQ
Q: Why does a matrix upload fail when every job uses the same artifact name?
A: GitHub Actions artifacts are immutable in the v4 model. The second job cannot append to the artifact created by the first, so the service returns a conflict. Add the matrix value to the name or upload unique artifacts and merge them later.
Q: Does overwrite: true combine files from multiple jobs?
A: No. It deletes a matching artifact before uploading a new one. Use it only when the replacement is sequential and intentional; use unique names plus actions/upload-artifact/merge when the inputs come from parallel jobs.
Q: Is a missing dotfile the same problem as an existing-artifact conflict?
A: No. Hidden-file exclusion controls which paths are included in an upload. An existing-artifact conflict means the final artifact identity is already taken. Diagnose the path contents and the artifact name separately.
References:
actions/upload-artifact README
Report a typo or broken link, or suggest a related topic.