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: 0 in 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_TOKEN instead 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. 1.Check the current rate limit status: See how much quota remains.
  2. 2.```yaml
  3. 3.- name: Check rate limit
  4. 4.run: |
  5. 5.curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
  6. 6.https://api.github.com/rate_limit | jq '.resources.core'
  7. 7.`
  8. 8.Implement rate limit awareness in API calls: Check before making requests.
  9. 9.```yaml
  10. 10.- name: Make API call with rate limit check
  11. 11.run: |
  12. 12.RATE_LIMIT=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
  13. 13.https://api.github.com/rate_limit | jq '.resources.core.remaining')
  14. 14.if [ "$RATE_LIMIT" -lt 50 ]; then
  15. 15.echo "Rate limit low ($RATE_LIMIT), waiting..."
  16. 16.sleep 300
  17. 17.fi
  18. 18.# Proceed with API call
  19. 19.gh api repos/${{ github.repository }}/issues
  20. 20.`
  21. 21.Use a personal access token for higher rate limits: Switch to a PAT.
  22. 22.```yaml
  23. 23.- name: Use PAT for API calls
  24. 24.env:
  25. 25.GH_TOKEN: ${{ secrets.PAT_TOKEN }} # Has 5,000 req/hour limit
  26. 26.run: |
  27. 27.gh api repos/${{ github.repository }}/releases
  28. 28.`
  29. 29.Batch API requests to reduce total calls: Minimize the number of API calls.
  30. 30.```yaml
  31. 31.# Instead of creating one issue per failure, batch them
  32. 32.- name: Create summary issue
  33. 33.run: |
  34. 34.# Collect all failures into one issue
  35. 35.gh issue create --title "Build failures summary" --body "$FAILURES"
  36. 36.`
  37. 37.Add exponential backoff for rate-limited requests: Handle 403 responses gracefully.
  38. 38.```yaml
  39. 39.- name: API call with retry
  40. 40.run: |
  41. 41.for i in 1 2 3 4 5; do
  42. 42.response=$(curl -s -w "%{http_code}" -o response.json \
  43. 43.-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
  44. 44."https://api.github.com/repos/${{ github.repository }}/issues")
  45. 45.if [ "$response" = "200" ] || [ "$response" = "201" ]; then
  46. 46.break
  47. 47.elif [ "$response" = "403" ]; then
  48. 48.echo "Rate limited, waiting 60 seconds..."
  49. 49.sleep 60
  50. 50.else
  51. 51.echo "Unexpected response: $response"
  52. 52.exit 1
  53. 53.fi
  54. 54.done
  55. 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