Codecov rate limit reached in GitHub Actions uploads
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.
['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')}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 |
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
- Open the Codecov step and find the
-> Token length:line. - A zero means no token was resolved; go and look for
Token set from envorToken set from inputabove it, and their absence is the answer. - 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.
- uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.infoUse 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.
permissions:
contents: read
id-token: write
steps:
- uses: codecov/codecov-action@v7
with:
use_oidc: trueDecide 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.
- uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: trueA 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.
- run: npm test -- --coverage
- uses: codecov/codecov-action@v7
with:
files: ./coverage/lcov.infoThe 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:
Branch `main` is protected but no token was provided
For information on Codecov upload tokens, see https://docs.codecov.com/docs/codecov-tokensA 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.
==> Failed to run upload-coverageWhen 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.
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_errordeliberately 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.
Frequently asked questions
What does "Expected time to availability" mean in a Codecov 429?
Why is my Codecov step green when the upload failed?
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?
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?
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.Related guides
References
- codecov/codecov-action: action.yml, the token, fork and input wiring
- codecov/codecov-action: dist/codecov.sh, the wrapper that prints the token length
- codecov/codecov-cli: services/commit, the TOKENLESS branch and the protected-branch warning
- codecov/codecov-action#1573: a 429 quoting the rate limit and the wait in seconds
- GitHub Actions documentation