# Codecov rate limit reached in GitHub Actions uploads

> Codecov rate limit reached in GitHub Actions means the upload carried no token. Read the token line first: the step stays green either way.

Source: https://latchkey.dev/learn/github-actions/codecov-action-token-required-rate-limit  
Updated: 2026-09-20

Codecov rate limit reached in GitHub Actions is what an anonymous upload gets when too many have come from the same place. The upload is throttled rather than rejected, the response carries the number of seconds until it would be allowed, and by default the step does not fail, so the first symptom is usually missing coverage rather than a red run.

## What this error means

Coverage stops appearing on Codecov while the workflow stays green. That combination is the signature: the action's `fail_ci_if_error` input defaults to the string `false`, so an upload error prints and the step continues. In the log the upload ends with a 429 and a message that names the token as the remedy and gives a wait in seconds. Two lines higher, the action has already printed how long the token it found was. If that reads zero, the upload went out anonymously and everything below it follows from that. Read that line before you read the error, because it is the one that separates a repository that has no token from one whose token is not reaching the step.

```Actions log, quoted from codecov/codecov-action#1573
['error'] There was an error running the uploader: Error uploading to https://codecov.io: Error: There was an error fetching the storage URL during POST: 429 - {'detail': ErrorDetail(string='Rate limit reached. Please upload with the Codecov repository upload token to resolve issue. Expected time to availability: 933s.', code='throttled')}
```

## Common causes

### The upload went out with no token and was throttled

The direct cause of the 429, and the one the message itself names. Anonymous uploads from a GitHub-hosted runner share address space with a great deal of other anonymous traffic, so the limit is reached far sooner than the volume of your own repository would suggest. The wait in the response is the service telling you when it would accept the upload, not how long the action will wait.

### A token exists but is not reaching the step

A secret renamed, a secret defined on an environment the job does not reference, or a fork pull request where secrets are withheld by design. The tell is identical in all three: a token length of zero. This is where most of the time goes, because the workflow file plainly passes a token and the log plainly says there is none.

### The step produced no coverage file to send

The upload is not throttled because it never happened. The CLI prints the count of files it found, and a zero there means the search pattern, the working directory or the test command is wrong rather than anything about tokens. Inherited from the older token page this one merges, where it was the second most common report.

### Nobody noticed because the step stayed green

Not a cause of the error so much as a cause of the delay in finding it. With `fail_ci_if_error` at its default of false, every one of the above prints and passes. In our experience a repository is usually several weeks into this before someone asks why the coverage graph is flat.

## How to fix it

### Read the token length before anything else

1. Open the Codecov step and find the `-> Token length:` line.
2. A zero means no token was resolved; go and look for `Token set from env` or `Token set from input` above it, and their absence is the answer.
3. A non-zero length means the token arrived, and a 429 after that is about the token being wrong or revoked rather than missing.

### Pass a repository upload token

Store the token as a secret and hand it to the action. Either route works, because the action checks the `CODECOV_TOKEN` environment variable before the `token` input, and both land in the same place.

```.github/workflows/ci.yml (illustrative)
- uses: codecov/codecov-action@v7
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
```

### Use OIDC instead of a stored secret

The first branch of the token chain mints a token from the job rather than reading one from your secrets, which removes the rotation problem entirely. It needs the `id-token: write` permission on the job, and the action skips it on a fork run, where no OIDC token would be issued anyway.

```.github/workflows/ci.yml (illustrative)
permissions:
      contents: read
      id-token: write
    steps:
      - uses: codecov/codecov-action@v7
        with:
          use_oidc: true
```

### Decide what a failed upload should do to the job

The default is to pass. If coverage is load-bearing for your merges, set `fail_ci_if_error` to true so a throttled upload is visible the day it happens rather than the week the gate breaks. If some jobs legitimately produce no coverage, set `handle_no_reports_found` to true as well, so an empty run is a log line rather than an exception.

```.github/workflows/ci.yml (illustrative)
- uses: codecov/codecov-action@v7
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true
```

## How to prevent it

- Pass a token on every repository, public included, so no upload depends on the anonymous allowance.
- Keep the token as a repository secret rather than an environment secret, unless the job already names that environment.
- Set `fail_ci_if_error` deliberately once, rather than leaving the default to decide for you.
- Assert the coverage file exists in the step before the upload, so an empty upload fails at the cause.

## Where the token comes from, in the order the action looks

The action resolves a token in one shell step called `Get and set token`, and the order is a chain of shell conditionals in its `action.yml`. Reading it settles most of these failures, because each branch announces itself in the log and the branch that ran tells you which input was actually seen. The table is that step, in order.

| Order | Condition in `action.yml` | What the log says | Result |
| --- | --- | --- | --- |
| 1 | `use_oidc` is true and the run is not a fork | nothing of its own | The OIDC token becomes `CC_TOKEN` |
| 2 | The `CODECOV_TOKEN` environment variable is set | `==> Token set from env` | That value becomes `CC_TOKEN` |
| 3 | The `token` input is set | `==> Token set from input` | That value, newlines stripped, becomes `CC_TOKEN` |
| 4 | None of the above | neither line appears | `CC_TOKEN` stays unset, and the upload is anonymous |

> Whichever branch ran, the wrapper then prints `-> Token length: N`. A zero there means branch four, no matter what the workflow file appears to say.

## A workflow that produces it

This file is written for this page and has never been run. It is a public repository uploading without a token, which the action permits and Codecov throttles. On a quiet repository it works for months, and on a busy one, or from a shared runner address, it starts returning 429 with a wait measured in the hundreds or thousands of seconds.

```.github/workflows/ci.yml (illustrative)
- run: npm test -- --coverage
      - uses: codecov/codecov-action@v7
        with:
          files: ./coverage/lcov.info
```

## The fork path is deliberate, and it changes the branch name

A pull request from a fork cannot see your secrets, so a tokenless upload there is the design rather than a misconfiguration. The action has a step named `Override branch for forks` that fires only when the branch override is empty, `CC_TOKEN` is empty and the run is a fork. It logs `==> Fork detected, setting branch to` and the pull request head label, then exports that label as both `CC_BRANCH` and an environment variable named `TOKENLESS`.

The CLI picks that up at the other end. In `codecov_cli/services/commit/__init__.py` the branch is replaced by the `TOKENLESS` value, and then the branch name itself is the signal: a head label is of the form owner colon branch, and the code checks for that colon. With one it logs `Creating a commit for an unprotected branch:`; without one, and with no token, it warns instead. That warning is worth quoting in full, because it is the closest thing to a token-required message the tool actually prints today:

```codecov-cli, codecov_cli/services/commit/__init__.py
Branch `main` is protected but no token was provided
For information on Codecov upload tokens, see https://docs.codecov.com/docs/codecov-tokens
```

> It is a warning, not an error. The CLI does carry a string that reads `Codecov token not found. Please provide Codecov token with -t flag.`, but nothing in the current upload path calls the function that raises it, so a log that says that is not what you will find.

## A green step is not a successful upload

The wrapper decides whether an error is fatal from one variable. `exit_if_error` prints the message and only exits 1 when `CC_FAIL_ON_ERROR` is `true`, and `action.yml` maps that variable to the `fail_ci_if_error` input, whose declared default is the string `false`. So the default behavior of this action is to report a failed upload and let the job pass.

That is a defensible default, because a coverage service being slow should not block a merge, but it means nobody notices until a coverage gate starts failing on a pull request weeks later. If you are hunting a repository where coverage has quietly stopped updating, search the logs for the wrapper's own failure line rather than for a red job.

```codecov-action, dist/codecov.sh
==> Failed to run upload-coverage
```

## When there was nothing to upload at all

The other half of this page's territory is an upload that never had a file. The CLI counts what it found and says so, logging `Found N coverage files to report`, and when that count is zero it raises with a fixed sentence: `No coverage reports found. Please make sure you're generating reports successfully.` That is a `ClickException`, so the CLI exits non-zero, and whether the step goes red still depends on `fail_ci_if_error`.

The action exposes an input for the opposite preference. Setting `handle_no_reports_found` to true swaps the exception for a log line reading `No coverage reports found. Triggering notifications without uploading.` and carries on. Choose deliberately: the first is right for a repository where coverage is required, the second for one where some jobs genuinely produce none.

## Why there is no recorded run on this page

The throttle lives at Codecov, and the number of seconds it returns depends on how much anonymous traffic has come from the address range your runner happened to get. A run of ours would record one draw from that distribution on one day, present it as a measurement, and teach a reader a number that is not theirs. Worse, reproducing it on demand would mean deliberately generating anonymous load against a service we do not run. The log above is quoted from an issue on the action's own repository, and the token chain is read from the action and CLI sources rather than reproduced.

## FAQ

### What does "Expected time to availability" mean in a Codecov 429?

It is the service telling you how many seconds until an anonymous upload from your address would be accepted. It is not a delay the action waits out; the upload has already failed by the time you read it. Supplying a repository upload token removes the anonymous allowance from the equation, which is what the message itself recommends.

### Why is my Codecov step green when the upload failed?

Because the wrapper only exits non-zero when `CC_FAIL_ON_ERROR` is true, and the action maps that to the `fail_ci_if_error` input, whose default is the string `false`. The failure is printed and the job continues. Set the input to true if a missed upload should be visible immediately.

### Do I need a Codecov token for pull requests from forks?

You cannot have one there: secrets are withheld from fork pull request runs. The action handles it by detecting the fork, logging that it is setting the branch to the head label, and exporting that label as `TOKENLESS` so the CLI treats it as an unprotected branch. Tokenless uploads from forks are throttled like any other.

### Where does "No coverage reports found" come from?

From the Codecov CLI, not from the action. Its collector counts the files it found, logs that count, and when it is zero raises `No coverage reports found. Please make sure you're generating reports successfully.` Setting the action's `handle_no_reports_found` input to true turns that into a log line and a notification instead.

## References

- [codecov/codecov-action: action.yml, the token, fork and input wiring](https://github.com/codecov/codecov-action/blob/main/action.yml)
- [codecov/codecov-action: dist/codecov.sh, the wrapper that prints the token length](https://github.com/codecov/codecov-action/blob/main/dist/codecov.sh)
- [codecov/codecov-cli: services/commit, the TOKENLESS branch and the protected-branch warning](https://github.com/codecov/codecov-cli/blob/main/codecov_cli/services/commit/__init__.py)
- [codecov/codecov-action#1573: a 429 quoting the rate limit and the wait in seconds](https://github.com/codecov/codecov-action/issues/1573)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
