Introduction
GitHub Actions workflows that make frequent API calls -- to create issues, manage releases, or query repository data -- can exceed the GitHub API rate limit. The default GITHUB_TOKEN has a rate limit of 1,000 requests per hour for authenticated requests. When this limit is exceeded, subsequent API calls return HTTP 403, causing workflow steps to fail.
Symptoms
- Workflow step fails with
HTTP 403: API rate limit exceeded - Error message includes
X-RateLimit-Remaining: 0in response headers - Workflow works when run manually but fails during bulk operations
- Multiple concurrent workflows competing for the same rate limit pool
- Error message:
{"message":"API rate limit exceeded for...","documentation_url":"https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting"}
Common Causes
- Workflow making API calls in a loop (e.g., creating issues for each failed test)
- Multiple workflows running concurrently, sharing the same rate limit
- Using
GITHUB_TOKENinstead of a personal access token with higher rate limit - Retry logic without rate limit awareness, burning through remaining quota
- Large repository with many API calls for status checks, PR management, etc.
Step-by-Step Fix
- 1.Check the current rate limit status: See how much quota remains.
- 2.```yaml
- 3.- name: Check rate limit
- 4.run: |
- 5.curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
- 6.https://api.github.com/rate_limit | jq '.resources.core'
- 7.
` - 8.Implement rate limit awareness in API calls: Check before making requests.
- 9.```yaml
- 10.- name: Make API call with rate limit check
- 11.run: |
- 12.RATE_LIMIT=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
- 13.https://api.github.com/rate_limit | jq '.resources.core.remaining')
- 14.if [ "$RATE_LIMIT" -lt 50 ]; then
- 15.echo "Rate limit low ($RATE_LIMIT), waiting..."
- 16.sleep 300
- 17.fi
- 18.# Proceed with API call
- 19.gh api repos/${{ github.repository }}/issues
- 20.
` - 21.Use a personal access token for higher rate limits: Switch to a PAT.
- 22.```yaml
- 23.- name: Use PAT for API calls
- 24.env:
- 25.GH_TOKEN: ${{ secrets.PAT_TOKEN }} # Has 5,000 req/hour limit
- 26.run: |
- 27.gh api repos/${{ github.repository }}/releases
- 28.
` - 29.Batch API requests to reduce total calls: Minimize the number of API calls.
- 30.```yaml
- 31.# Instead of creating one issue per failure, batch them
- 32.- name: Create summary issue
- 33.run: |
- 34.# Collect all failures into one issue
- 35.gh issue create --title "Build failures summary" --body "$FAILURES"
- 36.
` - 37.Add exponential backoff for rate-limited requests: Handle 403 responses gracefully.
- 38.```yaml
- 39.- name: API call with retry
- 40.run: |
- 41.for i in 1 2 3 4 5; do
- 42.response=$(curl -s -w "%{http_code}" -o response.json \
- 43.-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
- 44."https://api.github.com/repos/${{ github.repository }}/issues")
- 45.if [ "$response" = "200" ] || [ "$response" = "201" ]; then
- 46.break
- 47.elif [ "$response" = "403" ]; then
- 48.echo "Rate limited, waiting 60 seconds..."
- 49.sleep 60
- 50.else
- 51.echo "Unexpected response: $response"
- 52.exit 1
- 53.fi
- 54.done
- 55.
`
Prevention
- Use GitHub App tokens instead of PATs for even higher rate limits (5,000 per installation)
- Cache API responses in workflow artifacts to avoid repeated calls
- Monitor rate limit usage in workflows and alert when approaching limits
- Design workflows to minimize API calls -- use batch operations where possible
- Implement retry logic with exponential backoff and rate limit awareness
- Consider using the GraphQL API which can fetch more data in fewer requests