
The Problem
You’re running a GitHub Actions workflow and suddenly encounter this error:
Failed to CreateArtifact: Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.
Even more frustrating, you’ve already deleted old artifacts, but your workflows still fail with the same quota error. This article walks through why this happens and how to resolve it.
Understanding GitHub Storage Budgets
GitHub has two separate storage quotas that developers often confuse:
Actions Budget
- Purpose: Temporary CI/CD artifacts and logs
- Includes: Workflow artifacts, run logs, and cache
- Default retention: 90 days for artifacts, 7 days for cache
- Storage limits:
- Free tier: 500 MB
- Pro tier: 2 GB
- Additional storage: $0.25/GB/month
Packages Budget
- Purpose: Permanent package distribution
- Includes: Container images, npm packages, Maven artifacts
- Retention: Indefinite
- Storage limits:
- Free tier: 500 MB
- Pro tier: 2 GB
- Additional storage: $0.50/GB/month
The artifact upload error indicates you’ve hit your Actions budget limit, not your Packages budget.
Why Deleting Artifacts Doesn’t Work Immediately
Here’s the critical issue that catches most developers off-guard:
When you delete artifacts through the GitHub UI or API, the storage is freed immediately, but GitHub’s billing/quota system doesn’t recognize it for 6-12 hours.
This creates a frustrating situation where:
- ✅ Artifacts are actually deleted
- ✅ Storage is technically available
- ❌ Quota counter still shows old usage
- ❌ Workflows continue failing with quota errors
GitHub explicitly states: “Usage is recalculated every 6-12 hours.”
Solution 1: Manual Artifact Cleanup
Using GitHub CLI
For bulk deletion, the GitHub CLI is more efficient:
# Install GitHub CLI first
brew install gh # macOS
# or visit https://cli.github.com/
# Authenticate
gh auth login
# Delete all artifacts in a repository
gh api repos/{owner}/{repo}/actions/artifacts --paginate \
| jq -r '.artifacts[].id' \
| xargs -I {} gh api repos/{owner}/{repo}/actions/artifacts/{} -X DELETE
Without jq:
for id in $(gh api repos/{owner}/{repo}/actions/artifacts --paginate --jq '.artifacts[].id'); do
echo "Deleting artifact $id"
gh api repos/{owner}/{repo}/actions/artifacts/$id -X DELETE
done
Verify Deletion
# Check remaining artifacts
gh api repos/{owner}/{repo}/actions/artifacts
# Should return:
# {
# "total_count": 0,
# "artifacts": []
# }
Solution 2: Automated Artifact Cleanup Workflow
Create a GitHub Action to automatically clean up old artifacts on a schedule:
.github/workflows/cleanup-artifacts.yml:
name: Delete All Artifacts
on:
workflow_dispatch: # Manual trigger
schedule:
- cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC
jobs:
delete-artifacts:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
steps:
- name: Delete all artifacts
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const artifacts = await github.paginate(
github.rest.actions.listArtifactsForRepo,
{
owner,
repo,
per_page: 100
}
);
for (const artifact of artifacts) {
try {
await github.rest.actions.deleteArtifact({
owner,
repo,
artifact_id: artifact.id
});
} catch (error) {
// Continue on error
}
}
To run immediately:
- Go to Actions tab
- Select “Delete All Artifacts” workflow
- Click “Run workflow” button
- Select your branch and confirm
Solution 3: Set Artifact Retention Policies
The best long-term solution is preventing quota issues before they occur:
- uses: actions/upload-artifact@v4
with:
name: build-output
path: ./build
retention-days: 3 # Auto-delete after 3 days
Recommended retention periods:
- Development builds: 1-3 days
- Release candidates: 7 days
- Production builds: 14-30 days
Organization-Wide Settings
Set default retention for all repositories:
- Go to Organization Settings
- Navigate to Actions → General
- Set Artifact and log retention (1-90 days)
Solution 4: Disable Artifacts Temporarily
If you need to run workflows immediately while waiting for quota recalculation:
- uses: actions/upload-artifact@v4
if: false # Temporarily disable
with:
name: build-output
path: ./build
Or comment out the entire artifact upload step.
Solution 5: Use External Storage
For critical builds that can’t wait, upload to external storage:
AWS S3
- name: Upload to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 cp ./build s3://your-bucket/artifacts/${{ github.sha }}/ --recursive
Azure Blob Storage
- name: Upload to Azure
env:
AZURE_STORAGE_CONNECTION_STRING: ${{ secrets.AZURE_STORAGE_CONNECTION_STRING }}
run: |
az storage blob upload-batch \
-d artifacts \
-s ./build \
--connection-string "$AZURE_STORAGE_CONNECTION_STRING"
Solution 6: Deploy Directly Without Artifacts
The most efficient solution is to skip artifact storage entirely and deploy directly from the build step. This approach:
- Eliminates storage quota concerns
- Reduces workflow complexity
- Speeds up deployment by removing the upload/download cycle
- Works particularly well for mobile app stores, cloud deployments, and container registries
Mobile App Deployment (App Store/Play Store)
Instead of uploading build artifacts and downloading them in a separate deployment job, build and deploy in a single job:
Traditional approach (uses artifacts):
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build app
run: flutter build apk
- uses: actions/upload-artifact@v4 # Uses quota
with:
name: app-release
path: build/app/outputs/apk/release/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4 # Downloads from quota
with:
name: app-release
- name: Deploy to Play Store
run: fastlane supply
Direct deployment approach (no artifacts):
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
- name: Build APK
run: flutter build apk --release
- name: Deploy to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.example.app
releaseFiles: build/app/outputs/apk/release/app-release.apk
track: production
iOS App Deployment
jobs:
build-and-deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
- name: Build iOS
run: |
flutter build ios --release --no-codesign
cd ios
fastlane build
- name: Upload to TestFlight
run: |
cd ios
fastlane pilot upload
env:
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
Web Application Deployment
Deploy directly to hosting platforms without intermediate artifact storage:
Deploy to Azure Web App:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0'
- name: Build
run: dotnet publish -c Release -o ./publish
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: 'your-app-name'
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish
Container Registry Deployment
Build and push Docker images directly without storing them as artifacts:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
your-username/your-app:latest
your-username/your-app:${{ github.sha }}
Push to GitHub Container Registry:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
Cloud Storage Deployment
Upload built files directly to cloud storage:
Deploy to AWS S3 + CloudFront:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: |
npm install
npm run build
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Deploy to S3
run: |
aws s3 sync ./dist s3://your-bucket-name --delete
- name: Invalidate CloudFront
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
Multi-Platform Deployment
For apps that need to deploy to multiple platforms, combine builds in a single job:
jobs:
build-and-deploy-all:
runs-on: macos-latest # Supports iOS, Android, and web
steps:
- uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
# Android
- name: Build and deploy Android
run: |
flutter build apk --release
cd android
fastlane deploy_playstore
# iOS
- name: Build and deploy iOS
run: |
flutter build ios --release
cd ios
fastlane deploy_testflight
# Web
- name: Build and deploy Web
run: |
flutter build web --release
firebase deploy --only hosting
When to Use Direct Deployment
Use direct deployment when:
- You’re hitting storage quota limits regularly
- Build artifacts are only needed for immediate deployment
- You have a single deployment target per build
- Deployment happens in the same workflow as the build
- You want faster, simpler workflows
Keep artifacts when:
- Multiple jobs need the same build output
- You need to archive releases for compliance/auditing
- You want to manually test builds before deployment
- You have multiple deployment targets that run at different times
- You need to download builds for local testing
Monitoring Storage Usage
Check Usage via GitHub CLI
# For cache usage
gh api repos/{owner}/{repo}/actions/cache/usage
# For organization billing (shared storage)
gh api orgs/{org}/settings/billing/shared-storage
Check Usage via UI
- Navigate to repository Settings
- Click Actions → General
- Scroll down to view storage usage
API Response Format
{
"active_caches_size_in_bytes": 0,
"active_caches_count": 0
}
Understanding the Recalculation Timeline
When you delete artifacts:
| Time | Storage State | Quota State | Workflow Status |
|---|---|---|---|
| T+0 min | ✅ Freed | ❌ Still full | ❌ Fails |
| T+1 hour | ✅ Freed | ❌ Still full | ❌ Fails |
| T+6 hours | ✅ Freed | ⏳ Maybe updated | ⏳ May work |
| T+12 hours | ✅ Freed | ✅ Updated | ✅ Works |
There is no way to force immediate recalculation. You must either:
- Wait 6-12 hours
- Use workarounds (disable artifacts, external storage)
- Contact GitHub Support for manual recalculation (rare cases)
Checking When Recalculation Completes
Unfortunately, GitHub doesn’t provide notifications. Your options:
Method 1: Poll the API
# Check every few hours
gh api repos/{owner}/{repo}/actions/cache/usage
Method 2: Test Workflow
Create a minimal test workflow:
name: Test Quota
on: workflow_dispatch
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "test" > test.txt
- uses: actions/upload-artifact@v4
with:
name: quota-test
path: test.txt
Run it manually every few hours. When it succeeds, recalculation is complete.
Organization-Specific Considerations
If your repository belongs to an organization:
Permissions Required
Ensure your workflow has proper permissions:
permissions: actions: write contents: read
Organization Settings
- Organization Settings → Actions → General
- Enable “Allow GitHub Actions to create and approve pull requests”
- Set workflow permissions to “Read and write permissions”
Workflow Approval
Some organizations require admin approval for new workflows. If your cleanup workflow doesn’t run, check with your organization administrator.
Best Practices to Avoid Future Issues
1. Implement Aggressive Retention Policies
retention-days: 3 # For most builds
2. Clean Up Regularly
Schedule weekly artifact cleanup workflows.
3. Use Conditional Uploads
- uses: actions/upload-artifact@v4
if: github.event_name == 'release' # Only on releases
with:
name: production-build
path: ./build
4. Monitor Storage Usage
Set up alerts when approaching 75% of quota.
5. Consider Upgrading
If consistently hitting limits:
- Free → Pro: 500 MB → 2 GB
- Additional storage: $0.25/GB/month
Troubleshooting Checklist
When facing quota issues:
- [ ] Verify you’re checking the correct budget (Actions vs Packages)
- [ ] Confirm artifacts are actually deleted (
gh api repos/{owner}/{repo}/actions/artifacts) - [ ] Check cache usage separately (
actions/cache) - [ ] Wait 6-12 hours after deletion
- [ ] Verify organization permissions if applicable
- [ ] Test with minimal workflow
- [ ] Consider temporary workarounds (disable uploads, external storage)
- [ ] Implement retention policies for future prevention
Common Mistakes
Mistake 1: Deleting Workflow Runs Instead of Artifacts
Deleting the workflow run doesn’t delete artifacts. You must explicitly delete artifacts.
Mistake 2: Assuming Immediate Quota Updates
The 6-12 hour delay is not a bug—it’s how GitHub’s billing system works.
Mistake 3: Confusing Actions and Packages Budgets
These are separate quotas. Artifact uploads use Actions budget, not Packages.
Mistake 4: Not Setting Retention Policies
Without explicit retention settings, artifacts persist for 90 days by default.
Conclusion
GitHub Actions storage quota issues are frustrating because of the 6-12 hour recalculation delay. The key takeaways:
- Immediate fix: Delete artifacts, but expect a wait
- Short-term workaround: Disable artifact uploads or use external storage
- Long-term solution: Implement aggressive retention policies
- Best architectural solution: Deploy directly without artifacts when possible
- Prevention: Regular cleanup workflows and monitoring
By implementing proper retention policies, automated cleanup workflows, and considering direct deployment architectures, you can avoid hitting storage quotas in the future. The recalculation delay is unavoidable, but with proper planning and smart workflow design, it becomes a non-issue.
The direct deployment approach (Solution 6) is particularly valuable because it not only eliminates storage quota concerns entirely but also simplifies your CI/CD pipeline and speeds up your deployment process. For many use cases, artifacts are an unnecessary intermediate step that can be eliminated.