Inside the v4 artifact actions overhaul

GitHub has shipped v4 of its upload-artifact and download-artifact actions for GitHub Actions. The update rewrites the underlying storage and transfer architecture used by roughly four million artifacts created daily, with the stated goal of making uploads and downloads up to 10x faster, more reliable, and available sooner in a workflow run. Here’s what changed under the hood and what it means for workflow authors.

Why the previous versions hit their limits

The initial artifact actions (v1) were built by adapting the runner’s existing log upload service. That coupling tied artifact action releases to runner releases, so v2 rewrote the functionality in Node.js and packaged it as the @actions/artifact npm package. Versions v2 and v3 were functionally identical apart from a newer Node runtime, and all versions through v3 relied on the same internal, log-service-derived APIs.

That architecture worked for basic cases but produced several recurring problems:

  • Run-scoped availability: Artifacts weren’t finalized until the entire workflow run completed, so they couldn’t appear in the UI or REST API until then. Sharing artifacts across runs or using them in approval-gated processes required working around undocumented internal APIs.
  • Concurrent corruption: Multiple jobs could upload to the same named artifact, leading to accidental file overwrites in matrix scenarios and undefined behavior from concurrent writes to different chunks of the same file. Downloads were also prone to transient errors because zip archives were generated dynamically from loose files on each request.
  • Confusing sizes: Artifacts were stored as individual gzipped files, and the archive was assembled on demand. The reported size, the on-disk upload size, and the downloaded zip size could all diverge significantly depending on compressibility.
  • Slow transfers: Each file required its own round trip. Large, poorly compressible files wasted CPU on the runner (chunking and gzipping) and on the backend (decompressing and reassembling). Uploads with many small files slowed further because the backend created a reference per file. Self-hosted runners outside GitHub’s hosted compute environment felt this most acutely.

How v4 reworks the storage path

The core change in v4 is elimination of the intermediary proxy service between runner and blob storage. The backend now issues shared access signatures (SAS) directly for a specific blob path, giving the runner authenticated, scoped, direct access for uploads and read-only access for downloads. The same mechanism serves the runner, UI, and REST API.

With that proxy gone, all artifact content is assembled client-side into a single zip archive. The runner builds the archive in memory, streams files according to the upload specification, and sends chunked uploads straight to blob storage. This single-file model delivers several benefits:

  • File size is known at upload time, and a checksum can be computed as data streams through the runner.
  • Downloads are direct fetches of the stored zip rather than on-demand assembly, removing a whole class of transient corruption failures.
  • There’s an optional compression-level input to trade upload speed against archive size for content that compresses differently.

The performance effect is most visible in pathological cases: uploading a large node_modules tree, which previously incurred per-file overhead, is markedly faster. Internal and real-world tests show more than 10x improvement in both directions.

Artifacts now appear before the run ends

Because an artifact is finalized at upload time rather than at the end of the run, v4 artifacts are visible in the UI and through the REST API immediately. The upload-artifact action also exposes outputs for the artifact ID and a direct URL.

That unlocks workflow patterns that were previously awkward or impossible: inspecting build output from a job before an approval gate, then using that same artifact in a downstream release job. Artifact URLs can also be posted into pull request comments by bots for easy access.

Downloading from other runs and repositories

The download-artifact action gains two new inputs alongside its existing ones: repository and run-id. Combined with a github-token that has actions:read permission, a workflow can fetch artifacts produced by a different run in a different repository. Without an explicit token, the action defaults to downloading from the current run, including previous run attempts.

Consistent sizing — and a few tradeoffs

One reported-size story is now consistent. The size shown in the UI and API matches both the uploaded content and the downloaded archive, since there’s a single stored zip. Checksums are computed during upload on the runner and are slated for future exposure as artifact metadata.

Not everything improves without friction, and v4 introduces a few deliberate incompatibilities:

  • Version coupling: v4 uploads cannot be paired with v3-or-lower downloads. If you upload with actions/upload-artifact@v4, download with actions/download-artifact@v4.
  • No more multiple uploads to one artifact: v4 artifacts are immutable, and two v4 artifacts cannot share a name within the same run. Matrix jobs that previously appended to a common artifact must instead suffix the matrix attributes into each artifact name and later download them all into a single directory.
  • Per-job cap: A single job can produce at most 500 artifacts.
  • Self-hosted networking: With the proxy removed, self-hosted runners must be able to reach GitHub’s storage endpoints directly. The self-hosted runner documentation lists the required addresses.

Migration guidance for common scenarios is collected in the upload-artifact repository’s migration documentation.

Enterprise server availability

GitHub Enterprise Server (GHES) support for the v4 artifact actions is still in progress. For the latest status and timeline, track the GitHub public roadmap.

To dive deeper into GitHub Actions, consult the official documentation, or start building workflows today.