Skip to content
Latchkey

Composer "Failed to download X (403)" rate limited in CI

Composer requested a package archive from GitHub and the response was 403, usually because the shared CI IP hit GitHub's rate limit while unauthenticated. An OAuth token lifts the limit; caching dist archives avoids re-downloading.

What this error means

composer install fails with "Failed to download vendor/pkg from dist: ... The \"https://api.github.com/repos/...\" file could not be downloaded (HTTP/1.1 403 Forbidden)" and a rate-limit hint.

composer
Failed to download symfony/console from dist:
  The "https://api.github.com/repos/symfony/console/zipball/abc123" file could not be
  downloaded (HTTP/1.1 403 Forbidden)
  It is possible that you are throttled by GitHub. Create an OAuth token to go over the rate limit.

Common causes

Anonymous GitHub downloads were rate limited

Without a token, GitHub limits requests per IP; shared CI runners hit the cap and archive downloads return 403.

No dist cache, so every job re-downloads

A cold Composer cache fetches every archive again, multiplying API calls and the chance of throttling.

How to fix it

Authenticate to lift the rate limit

Provide a GitHub token via COMPOSER_AUTH so archive downloads use the authenticated, higher quota.

.github/workflows/ci.yml
env:
  COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.COMPOSER_GH_TOKEN }}"}}'

Prefer dist and cache it

Install from dist archives and persist the Composer cache so repeat runs avoid GitHub entirely.

.github/workflows/ci.yml
- uses: actions/cache@v4
  with:
    path: ~/.cache/composer
    key: composer-${{ hashFiles('composer.lock') }}

How to prevent it

  • Always authenticate Composer to GitHub in CI.
  • Cache ~/.cache/composer between runs to cut download volume.
  • Use --prefer-dist so archives, not full clones, are fetched.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →