The GitHub Actions free tier includes 500MB of artifact storage (GitHub's billing documentation covers the breakdown by storage type). I hit that limit last week. The symptom was silent: an actions/upload-artifact step that completed successfully in previous runs started failing without a clear error in the workflow log. Checking the repo's storage usage under Settings → Billing showed 498MB used across artifact runs.
The source was obvious once I looked: the daily YouTube Short generation pipeline uploads rendered video files, audio segments, and slide frames on every run, with a 14-day retention window. At 20-30MB per successful run and two runs per day, it accumulates fast.
Here are the four changes I made to get back under the limit.
Move 1: Cut retention from 14 days to 2 days
The immediate fix. Change retention-days: 14 to retention-days: 2 everywhere.
- uses: actions/upload-artifact@v4
with:
name: yt-short-${{ github.run_number }}
path: output/
retention-days: 2
This alone cut storage by around 85% because I had weeks of large video artifacts accumulating. 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.
Move 2: Upload artifacts only on failure
The second change was adding if: failure() to most artifact upload steps. A pipeline that succeeds 95% of the time doesn't need to upload 25MB of artifacts on every successful run.
- uses: actions/upload-artifact@v4
if: failure()
with:
name: debug-on-fail-${{ github.run_number }}
path: |
output/
logs/
retention-days: 2
For the final rendered video specifically, I kept the upload unconditional — I want the MP4 available for the 24 hours after generation in case the YouTube upload step fails and I need to re-submit it manually. But intermediate audio segments, slide frame images, and build logs? Those only upload on failure now.
The tradeoff is acceptable: if I need to debug a successful run, I can re-run it. Artifacts from failed runs are what actually need investigation.
Move 3: Exclude heavy intermediate files from artifact paths
The video pipeline generates individual PNG slide frames before assembling the final video — eight slide types, each 1920×1080, roughly 2-4MB per frame. With 6-8 slides per Short, each run was producing 12-32MB of intermediate PNG files that I was uploading and never looking at.
The fix was an exclude pattern in the artifact path:
- uses: actions/upload-artifact@v4
if: failure()
with:
name: debug-${{ github.run_number }}
path: |
output/
!output/frames/
!output/*.wav
retention-days: 2
The ! prefix excludes matching paths. output/frames/ drops the individual slide PNGs; !output/*.wav drops the raw audio segment files that get merged into the final audio track.
When I'm debugging a failed run, the artifacts I actually need are: the final assembled MP4 (or partial MP4 if assembly failed), the script JSON that drove the generation, and the ffmpeg error log. The intermediate frames and WAV files are only useful if I'm diagnosing a frame-level rendering issue — rare enough that I'd rather re-run the pipeline at that point.
Move 4: Use actions/cache for dependencies instead of upload-artifact
This one was a mistake I'd made earlier in the monorepo setup: caching pnpm and Python dependencies using actions/upload-artifact instead of actions/cache. The two look similar in YAML but use different storage pools.
actions/upload-artifact consumes from the 500MB artifact pool. actions/cache uses a separate 10GB cache pool that doesn't count against the artifact quota:
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ~/.pnpm-store
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: pnpm-
- name: Cache Python packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${{ hashFiles('requirements.txt') }}
restore-keys: pip-
Moving dependencies from upload-artifact to cache freed up another 40-50MB per workflow run that had been quietly piling up from cached dependency snapshots. The cron scheduling patterns didn't change — same five workflows, same schedule — just the storage pool.
Where things stand
After these four changes, the monorepo runs at under 80MB of artifact storage on a typical day, with peaks around 150MB when multiple pipeline runs fail on the same day and each uploads its debug artifact. The 500MB limit is no longer a concern at current pipeline volume.
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 — I'll need to revisit retention and exclude patterns again. The current setup works for two video uploads per day. At five or six, even with 2-day retention, the numbers would climb back toward the limit.
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)