Troubleshooting GitHub Actions Storage Quota Issues

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:

  1. ✅ Artifacts are actually deleted
  2. ✅ Storage is technically available
  3. ❌ Quota counter still shows old usage
  4. ❌ 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:

  1. Go to Actions tab
  2. Select “Delete All Artifacts” workflow
  3. Click “Run workflow” button
  4. 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:

  1. Go to Organization Settings
  2. Navigate to ActionsGeneral
  3. 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

  1. Navigate to repository Settings
  2. Click ActionsGeneral
  3. 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:

TimeStorage StateQuota StateWorkflow 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

  1. Organization SettingsActionsGeneral
  2. Enable “Allow GitHub Actions to create and approve pull requests”
  3. 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:

  1. Immediate fix: Delete artifacts, but expect a wait
  2. Short-term workaround: Disable artifact uploads or use external storage
  3. Long-term solution: Implement aggressive retention policies
  4. Best architectural solution: Deploy directly without artifacts when possible
  5. 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.

Additional Resources

Posted in XAF

Leave a Reply

Your email address will not be published.