Skip to main content

GitHub API rate limit: pin to the toolcache, pass a token

Some actions call api.github.com while they run. setup-go, setup-node and setup-python ask it for a list of available versions whenever the version you named is not already on the runner. setup-java does the same only for distributions whose version list is hosted on GitHub, such as the Microsoft Build of OpenJDK; Temurin comes from Adoptium. setup-dotnet installs .NET from Microsoft’s own feed and is not affected. Other actions read releases or files from GitHub repositories.

GitHub allows 60 unauthenticated requests per hour per IP address. Hosted runners share outbound IP addresses, so other jobs use the same allowance as yours. When it runs out, the step fails:

Error: API rate limit exceeded for 80.158.x.x. (But here's the good news:
Authenticated requests get a higher rate limit. Check out the documentation
for more details.)

Two changes stop this. Do the first in every workflow; do the second for any action that still has to reach GitHub.

1. Pin a major.minor version, so the toolcache answers

Hosted runners carry specific versions of Node.js, Go, Python, Java and .NET in their toolcache: the list is on what the runner image contains. A setup-* action that finds the version there uses it directly and makes no call to GitHub.

It only finds it if you ask for a range the toolcache can satisfy. A full patch version (1.27.2) misses as soon as the runner image moves to 1.27.3, and the action then asks api.github.com for its version list and downloads from GitHub:

# Misses the toolcache when the image has a newer patch, then calls api.github.com
- uses: actions/setup-go@v5
  with:
    go-version: '1.27.2'

# Resolves from the toolcache: no GitHub call
- uses: actions/setup-go@v5
  with:
    go-version: '1.27'

The same applies to node-version: '24', python-version: '3.13' and the other setup-* inputs. Leave check-latest unset (it defaults to false): with check-latest: true the action asks GitHub for the newest version on every run, even when the toolcache has one.

A go.mod, .nvmrc or .python-version file read through go-version-file and similar inputs usually names an exact version, so it misses the toolcache in the same way. Name the major.minor version in the workflow instead.

2. Pass a GitHub.com token to actions that call GitHub

Authenticated requests get 5,000 per hour, counted against your token rather than the shared IP address. Create a token on GitHub.com and give it to the action:

  1. On GitHub.com, create a fine-grained personal access token with Public repositories (read-only) access and no other permissions. It only needs to read public data.
  2. In fremforge, store it as a repository or organisation secret, for example GH_DOTCOM_TOKEN.
  3. Pass it to the action’s token input:
- uses: actions/setup-node@v4
  with:
    node-version: '24'
    token: ${{ secrets.GH_DOTCOM_TOKEN }}

setup-go and setup-python take the same token input. So does setup-java, where it only matters for the distributions above. setup-dotnet has no token input and does not need one. Other actions that call GitHub usually accept token or github-token; check the action’s documentation.

secrets.GITHUB_TOKEN does not work here

On fremforge, secrets.GITHUB_TOKEN (and github.token) is the job’s Forgejo token. It authenticates against fremforge, not against github.com, so passing it to an action that calls api.github.com changes nothing: the request still counts as unauthenticated. Use a token created on GitHub.com, as above.

What fremforge does, and does not do

  • The runner image ships the versions of Node.js, Go, Python, Java and .NET listed on the runner image page in its toolcache. A workflow that asks for one of them as in step 1 never needs GitHub for it. A version outside that list is downloaded, and for setup-go, setup-node and setup-python that starts with a call to api.github.com.
  • Actions themselves (uses: actions/setup-go@v5 and the like) are fetched from fremforge’s own mirrors, not from GitHub, so resolving an action never counts against GitHub’s limit. See Marketplace compatibility.
  • fremforge does not add outbound IP addresses to spread GitHub’s per-IP limit. The two changes above remove the need, and they keep working whatever the IP address.