DEV Community

Cover image for Four GitHub Actions artifact storage moves after hitting the 500MB free quota
MORINAGA
MORINAGA

Posted on Edited on

Four GitHub Actions artifact storage moves after hitting the 500MB free quota

The GitHub Actions free tier includes 500MB of artifact storage (GitHub's billing documentation covers the breakdown by storage type). I hit that limit this week. The symptom was quiet rather than loud: actions/upload-artifact steps that had completed fine in previous runs started failing, non-fatally, without an obvious error in the workflow log. Checking the repo's storage usage under Settings → Billing showed the artifact quota effectively full.

The source was obvious once I looked: the video generation workflows each upload the rendered MP4 at the end of a run with a 14-day retention window. Three workflows do this — the daily Shorts cron, the long-form one, and the samurai-princess one — and 26 unexpired artifacts were sitting there totalling 438MB. At roughly one video a day, 14 days of retention is enough on its own to fill the quota.

Here is what I actually changed, and the three levers I expected to need that turned out not to apply.

The fix: cut retention from 14 days to 2 days

The whole change was retention-days: 14 → retention-days: 2 in the three upload steps, plus deleting the 26 artifacts that were already sitting there.

- uses: actions/upload-artifact@v4
  continue-on-error: true
  if: steps.pick.outputs.skip != 'true' && steps.pipeline.outputs.mp4_path != ''
  with:
    name: yt-video-${{ steps.pipeline.outputs.video_id }}
    path: ${{ steps.pipeline.outputs.mp4_path }}
    retention-days: 2
    if-no-files-found: warn
Enter fullscreen mode Exit fullscreen mode

Deleting the existing 26 artifacts is what freed the 438MB immediately; the retention change is what keeps it from refilling — at a video a day, the steady-state pile is 2 days deep instead of 14. The practical tradeoff: I can no longer recover a video file from a run that happened three days ago without re-running the pipeline. In practice I've never needed to do that — generated content goes directly to a publish queue, and once it's published the artifact is irrelevant.

The reason I had 14-day retention in the first place was vague defensiveness: "what if I need to debug something from last week?" Two days captures the window where debugging is actually likely. More than that is storage debt that doesn't pay off.

The three levers that didn't apply

Going in, I assumed the retention cut would be one of four changes. The other three are the standard advice for this problem, and none of them had anything to bite on in this repo. That's worth writing down, because "the obvious optimization doesn't apply" is a real outcome and it's the one that gets left out of posts like this.

Upload only on failure. The usual move is to wrap debug artifacts in if: failure() so a pipeline that succeeds most of the time isn't paying storage on every green run:

- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: debug-on-fail-${{ github.run_number }}
    path: logs/
    retention-days: 2
Enter fullscreen mode Exit fullscreen mode

That doesn't fit here. The single artifact each workflow uploads is the finished MP4, and the case I want it for is the successful render — a same-day copy I can repost by hand — so if: failure() never went in. What the artifact steps carry instead is a guard on the queue pick plus continue-on-error: true, so a full quota can't fail the job and strand the queue file.

Worth being precise about what that leaves uncovered, though. Because those if: expressions contain no status function, GitHub still applies the implicit success check, so a run where the YouTube upload fails skips the artifact step and keeps nothing. Preserving a successfully rendered MP4 on that path would take two changes: separating render from upload — the long-form workflow already does, while the Shorts and samurai-princess ones render and upload inside one main.sh step — and running the artifact step under always() against a path that is still set when the upload leg fails.

Exclude heavy intermediates from the artifact path. actions/upload-artifact supports !-prefixed exclude patterns, which is the right tool when you're uploading a whole output directory. But the upload path in all three workflows is a single MP4 file, not output/ — ${{ steps.pipeline.outputs.mp4_path }} in the Shorts and samurai-princess workflows, ${{ steps.render.outputs.mp4 }} in the long-form one. And the Shorts renderer doesn't produce the heavy intermediates I half-expected to find: visuals.sh writes slide_NNN.txt chunks that ffmpeg draws as text overlays directly into the 1080×1920 render, so there are no per-slide PNGs on disk to exclude in the first place.

Move dependencies from upload-artifact to actions/cache. This is a genuine trap — the two look similar in YAML but draw on different pools, with upload-artifact consuming the 500MB artifact quota and actions/cache a separate, larger cache pool. It just wasn't a mistake I'd made: dependency caching in this repo runs through setup-node's built-in cache: pnpm, and nothing in any workflow ever put a dependency directory into an artifact. Python deps aren't cached at all yet, which costs install minutes but not storage.

The one thing worth generalizing from a null result: before optimizing artifact storage, read the actual path: values. Three of my four planned changes were aimed at an upload shape this repo doesn't have.

Where things stand

The 500MB limit isn't a concern at current volume. With 2-day retention on roughly one video a day, the expected peak is about 80MB — the level the workflow comments now record, so the next person to touch retention can see what the number was based on.

The thing I'd do differently: check storage usage weekly from the start. GitHub doesn't send a notification when you approach the limit — you find out when artifact upload steps start failing silently. I now check the billing dashboard as part of weekly infrastructure review, the same way I check YouTube Studio and Cloudflare Analytics.

One remaining question: if the pipeline volume increases — more sites, more Shorts per day — retention will need revisiting again. The current setup is sized for about one video upload a day plus occasional queue-triggered long-form runs. At five or six a day, even 2-day retention would climb back toward the limit, and that's the point where the cron cadence and the storage budget stop being separate questions.


Part of an ongoing 6-month experiment running three AI-curated directory sites. The technical claims here are real; this article was AI-assisted.

Top comments (0)