GitHub API Rate Limit Errors: Fix 401, 403, 429, 502 & Timeout (2024 Guide)
Diagnose and fix GitHub API rate limit errors including 401, 403, 429, 502, and timeouts. Step-by-step commands, token strategies, and retry logic included.
- HTTP 429 and 403 errors from the GitHub API most often mean you have exhausted your hourly or secondary rate limit; unauthenticated requests are capped at 60/hour while authenticated ones get 5,000/hour.
- A 401 Unauthorized response means your Personal Access Token (PAT) or GitHub App installation token is missing, expired, revoked, or lacks the required scope — the token itself is the problem, not the rate limit.
- A 502 Bad Gateway or timeout during heavy batch operations usually means GitHub's backend is under load or your request hit a secondary rate limit on concurrent connections; exponential back-off with jitter and the Retry-After header are the canonical fix.
- Quick fix summary: authenticate every request with a valid token, inspect X-RateLimit-* response headers to understand your quota, implement exponential back-off, cache responses with ETags, and distribute load across multiple GitHub App installations if you need more than 5,000 requests/hour.
| Method | When to Use | Time to Implement | Risk |
|---|---|---|---|
| Add/rotate PAT token | 401 Unauthorized or missing token | 5 minutes | Low — standard credential rotation |
| Check token scopes | 403 on a specific resource (repo, org) | 5 minutes | Low — read-only settings change |
| Respect Retry-After header | 429 secondary rate limit or 503 | 1–2 hours | Low — purely additive logic |
| Exponential back-off with jitter | Any transient 5xx or 429 | 2–4 hours | Low — standard resilience pattern |
| ETags and conditional requests | Repeated reads of the same resource | 2–4 hours | Low — reduces quota consumption |
| GitHub App with multiple installations | Need >5,000 req/hour sustained | 1–2 days | Medium — requires App registration |
| GraphQL batching | Many small REST calls | 4–8 hours | Medium — query language change |
| Self-hosted GitHub Enterprise | Unlimited API calls behind firewall | Days–weeks | High — infrastructure investment |
Understanding GitHub API Rate Limit Errors
GitHub enforces two distinct rate-limit systems that developers routinely confuse:
Primary rate limits are per-user or per-app quotas reset on a rolling hourly window:
- Unauthenticated: 60 requests/hour (keyed on source IP)
- Authenticated PAT or OAuth: 5,000 requests/hour
- GitHub App installation token: 5,000 requests/hour per installation (15,000 for orgs on Enterprise plans)
- GitHub Actions
GITHUB_TOKEN: 1,000 requests/hour per repository
Secondary rate limits are request-rate or concurrency caps designed to prevent abuse:
- No more than 100 concurrent requests
- No more than 900 requests per minute to a single endpoint
- No more than 90 seconds of CPU time per 60-second window
- Content-creation endpoints (issues, comments, pull requests) have additional mutation limits
When you exceed either type of limit, GitHub returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 60
x-ratelimit-limit: 5000
x-ratelimit-remaining: 0
x-ratelimit-reset: 1708723200
x-ratelimit-used: 5000
x-ratelimit-resource: core
For secondary limits the body is often:
{"message": "You have exceeded a secondary rate limit. Please wait a few minutes before you try again.", "documentation_url": "https://docs.github.com/rest/overview/rate-limits"}
Understanding Each HTTP Status Code
401 Unauthorized
The request contained no credentials or the token was rejected outright.
Exact error body: {"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}
Common causes: token expired, token revoked by an org admin, wrong environment variable, encoding artifact (trailing newline in $GITHUB_TOKEN).
403 Forbidden
Authentication succeeded but authorization failed — either a scope is missing or a primary rate limit was hit (GitHub uses 403 for primary rate limits in some API versions).
Exact error body for rate limit: {"message": "API rate limit exceeded for user ID 12345.", "documentation_url": "https://docs.github.com/rest/overview/rate-limits"}
Exact error body for scope: {"message": "Resource not accessible by integration", "documentation_url": "..."}
429 Too Many Requests
Secondary rate limit triggered. Always contains a Retry-After header. Never retry immediately — wait for the value in that header (seconds).
502 Bad Gateway GitHub's load balancers returned an error before your request reached the API server. This happens during GitHub incidents or when your payload is abnormally large (>100 MB push, malformed GraphQL query). Check https://www.githubstatus.com/ first.
Timeout (no HTTP response) Network-level timeout. Causes include: DNS failure, TLS handshake timeout behind a corporate proxy, or GitHub's servers taking >30 seconds on a heavy search query.
Step 1: Diagnose — Read the Headers
The fastest diagnosis is to make a single authenticated request and inspect the rate-limit headers:
curl -si -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/rate_limit | grep -E "HTTP|x-ratelimit|Retry-After"
Key fields to examine:
x-ratelimit-remaining: if 0, you are rate-limitedx-ratelimit-reset: Unix timestamp when the window resetsx-ratelimit-resource: which bucket was exhausted (core,search,graphql,code_search)Retry-After: seconds to wait before retrying (secondary limit)
For a 401, check the token itself:
curl -si -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/user | head -5
# Look for: HTTP/2 200 (valid) vs HTTP/2 401 (invalid)
For a 403 scope error, decode what scopes your token has:
curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/user | grep x-oauth-scopes
# Example output: x-oauth-scopes: repo, read:org
Step 2: Fix Based on Diagnosis
Fix 401 — Token issues
- Regenerate your PAT at https://github.com/settings/tokens
- Ensure fine-grained PAT has the correct resource permissions (Contents: Read, Metadata: Read, etc.)
- Strip any whitespace:
export GITHUB_TOKEN=$(echo -n "$GITHUB_TOKEN" | tr -d '\n') - For GitHub Apps, ensure the installation token is refreshed before each job (they expire after 1 hour)
Fix 403 — Scope or primary rate limit
- Scope: add the missing OAuth scope to your token (e.g.,
read:orgfor organization data) - Primary rate limit: switch from unauthenticated to authenticated requests immediately; if already authenticated, implement request caching with ETags
Fix 429 — Secondary rate limit
Implement a retry loop that reads Retry-After:
import time, requests
def github_request(url, token, max_retries=5):
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
for attempt in range(max_retries):
r = requests.get(url, headers=headers)
if r.status_code == 429:
wait = int(r.headers.get("Retry-After", 60))
print(f"Secondary rate limit hit, waiting {wait}s")
time.sleep(wait)
continue
if r.status_code == 403 and r.json().get("message", "").startswith("API rate limit"):
reset = int(r.headers.get("x-ratelimit-reset", time.time() + 60))
wait = max(reset - int(time.time()), 1)
print(f"Primary rate limit hit, waiting {wait}s until reset")
time.sleep(wait)
continue
r.raise_for_status()
return r
raise RuntimeError("Max retries exceeded")
Fix 502 / Timeout — Transient failures Add exponential back-off with jitter for all 5xx responses:
import random, time
def backoff_wait(attempt, base=1, cap=64):
# Full jitter: random between 0 and min(cap, base * 2^attempt)
return random.uniform(0, min(cap, base * (2 ** attempt)))
Fix — Scale beyond 5,000 req/hour
- Create a GitHub App instead of using a PAT
- Install it on multiple organizations or user accounts
- Each installation gets its own 5,000 req/hour quota
- Round-robin requests across installation tokens
Fix — Reduce consumption with ETags
# First request: capture ETag
curl -si -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/repos/owner/repo/issues \
| grep etag
# Subsequent requests: use If-None-Match; a 304 costs 0 quota
curl -si -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "If-None-Match: \"abc123\"" \
https://api.github.com/repos/owner/repo/issues
# HTTP 304 Not Modified = free response, still returns cached data
Step 3: Prevent Recurrence
- Monitor your quota proactively: poll
/rate_limitand emit metrics to Prometheus or Datadog - Use GraphQL for batch reads: one GraphQL request can fetch data that would require dozens of REST calls
- Paginate efficiently: always use
per_page=100and use cursor-based pagination for GraphQL - Cache aggressively: store responses with their ETag; re-validate before each use
- Separate token pools by function: use one PAT for CI, another for bots, another for developer tooling to avoid contention
- Set request timeouts: always set a socket timeout (30s recommended) to avoid hanging goroutines/threads that consume connection slots
Frequently Asked Questions
#!/usr/bin/env bash
# github-api-diag.sh — Diagnose GitHub API rate limit and auth issues
# Usage: GITHUB_TOKEN=ghp_xxx ./github-api-diag.sh [owner/repo]
set -euo pipefail
API="https://api.github.com"
TOKEN="${GITHUB_TOKEN:-}"
REPO="${1:-}"
if [[ -z "$TOKEN" ]]; then
echo "ERROR: GITHUB_TOKEN is not set. Export it before running this script."
exit 1
fi
echo "=== 1. Validate token (check for 401) ==="
HTTP_STATUS=$(curl -so /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"$API/user")
echo "GET /user -> HTTP $HTTP_STATUS"
if [[ "$HTTP_STATUS" == "401" ]]; then
echo "FAIL: Token is invalid or expired. Regenerate at https://github.com/settings/tokens"
exit 1
fi
echo ""
echo "=== 2. Inspect token scopes ==="
curl -sI -H "Authorization: Bearer $TOKEN" "$API/user" \
| grep -E "x-oauth-scopes|x-accepted-oauth-scopes" || echo "(No OAuth scope headers — may be a fine-grained PAT)"
echo ""
echo "=== 3. Check all rate limit buckets ==="
curl -s -H "Authorization: Bearer $TOKEN" "$API/rate_limit" \
| python3 -c "
import sys, json
data = json.load(sys.stdin)
for bucket, info in data['resources'].items():
remaining = info['remaining']
limit = info['limit']
reset = info.get('reset', 0)
import datetime
reset_time = datetime.datetime.utcfromtimestamp(reset).strftime('%H:%M:%S UTC')
status = 'OK' if remaining > 0 else 'EXHAUSTED'
print(f'{bucket:20s}: {remaining:5d}/{limit} remaining reset={reset_time} [{status}]')
"
if [[ -n "$REPO" ]]; then
echo ""
echo "=== 4. Test repo access for $REPO ==="
HTTP_REPO=$(curl -so /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
"$API/repos/$REPO")
echo "GET /repos/$REPO -> HTTP $HTTP_REPO"
if [[ "$HTTP_REPO" == "403" ]]; then
echo "FAIL: 403 on repo. Check token has 'repo' or 'Contents: Read' scope."
elif [[ "$HTTP_REPO" == "404" ]]; then
echo "FAIL: 404 — repo does not exist or token cannot see it (private repo needs 'repo' scope)."
fi
fi
echo ""
echo "=== 5. Simulate a safe request with ETag caching ==="
ETAG=$(curl -sI -H "Authorization: Bearer $TOKEN" "$API/repos/github/docs" \
| grep -i '^etag:' | awk '{print $2}' | tr -d '\r')
echo "Captured ETag: $ETAG"
if [[ -n "$ETAG" ]]; then
REVAL_STATUS=$(curl -so /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
-H "If-None-Match: $ETAG" \
"$API/repos/github/docs")
echo "Re-validation request -> HTTP $REVAL_STATUS (304 = cached, quota-free)"
fi
echo ""
echo "=== Done. If exhausted, check x-ratelimit-reset timestamp above to know when quota refills. ==="Error Medic Editorial
The Error Medic Editorial team is composed of senior DevOps and SRE engineers with experience running high-volume integrations against GitHub, GitLab, and other developer APIs at scale. We write evidence-based troubleshooting guides rooted in real production incidents, official API documentation, and open-source community findings.
Sources
- https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/rate-limits-for-github-apps
- https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api
- https://stackoverflow.com/questions/14792002/github-api-rate-limit-how-to-get-rate-limit-info
- https://github.com/octokit/plugin-throttling.js
- https://docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api
- https://www.githubstatus.com/