GitHub API Rate Limit Errors: Fix 401, 403, 429, 502 & Timeout Issues
Diagnose and fix GitHub API rate limit, 401 unauthorized, 403 forbidden, 429 too many requests, 502 bad gateway, and timeout errors with step-by-step commands.
- HTTP 429 and some 403 responses both signal rate limiting — primary limits (5000 req/hr authenticated) vs secondary limits (burst/concurrency) require different fixes
- HTTP 401 means a missing, expired, or malformed Authorization header; HTTP 403 can mean either insufficient token scope or a secondary rate limit hit
- HTTP 502 and timeouts are usually transient GitHub infrastructure issues but can be triggered by extremely large response payloads or GraphQL query complexity
- Quick fix summary: authenticate all requests (eliminates 60 req/hr anonymous limit), cache ETag/conditional requests, implement exponential backoff on Retry-After header, and switch to GraphQL to batch queries
| Method | When to Use | Time to Implement | Risk |
|---|---|---|---|
| Add/rotate Personal Access Token | HTTP 401 or anonymous 403/429 | 5 minutes | Low — straightforward credential swap |
| Respect Retry-After / x-ratelimit-reset header | HTTP 429 or secondary-rate-limit 403 | 1–2 hours | Low — standard backoff pattern |
| Conditional requests (ETag / If-None-Match) | Repeated reads on same resource | 2–4 hours | Low — reduces quota consumption up to 100% |
| GitHub App + installation tokens | High-volume production pipelines (up to 15,000 req/hr per org) | 1–2 days | Medium — OAuth app registration required |
| Switch REST to GraphQL batching | Many round-trips for related resources | 1–3 days | Medium — query complexity limits apply |
| Distribute load across multiple tokens/orgs | Sustained throughput beyond single-token limits | 1–2 days | Medium — token rotation logic required |
| Increase client timeout / chunked pagination | HTTP 502 or read timeout on large repos | 2–4 hours | Low — pagination is always safer than large requests |
Understanding GitHub API Rate Limit and HTTP Error Codes
The GitHub REST API enforces several distinct rate-limiting tiers, and each tier surfaces a different HTTP status code. Conflating them leads to wasted debugging time. This guide walks through every error code in the cluster — 401, 403, 429, 502, and timeout — with concrete diagnostic commands and reproducible fixes.
HTTP 401 Unauthorized
Exact error message:
{"message": "Requires authentication", "documentation_url": "https://docs.github.com/rest"}
or
{"message": "Bad credentials", "documentation_url": "https://docs.github.com/rest"}
Root causes:
- No
Authorizationheader present (anonymous request) - Token has been revoked, expired (fine-grained PATs support expiration), or deleted
- Token string is malformed — common culprit is a trailing newline from
echo $TOKENvsprintf $TOKEN - Using a GitHub App JWT that has expired (JWTs are valid for 10 minutes max)
Fix — Step 1: Verify the token is present and well-formed
Always use printf or strip trailing newlines when reading tokens from environment or files:
TOKEN=$(cat ~/.github_token | tr -d '\n')
curl -s -H "Authorization: Bearer $TOKEN" https://api.github.com/user
A 200 response with your user object confirms the token is valid.
Fix — Step 2: Check token scopes
curl -sI -H "Authorization: Bearer $TOKEN" https://api.github.com/user \
| grep -i x-oauth-scopes
The x-oauth-scopes header lists granted scopes. If it is empty, the token has no scopes — regenerate it with at least repo for private repository access.
HTTP 403 Forbidden
This is the most ambiguous code because GitHub uses 403 for two completely different problems.
Case A — Insufficient token scope or repository permission:
{"message": "Must have admin rights to Repository.", "documentation_url": "..."}
Fix: regenerate the token with appropriate scopes (admin:repo_hook, write:packages, etc.) or ask a repository admin to grant collaborator access.
Case B — Secondary rate limit (burst/concurrency):
{"message": "You have exceeded a secondary rate limit. Please wait a few minutes before you retry your request.", "documentation_url": "..."}
Secondary limits exist to prevent abusive request patterns: more than ~100 concurrent requests, more than 900 points/minute on the REST API, or more than 2000 points/minute on GraphQL. These limits are not reflected in the x-ratelimit-remaining header.
Fix — Distinguish the two 403 types programmatically:
import httpx, time, re
def github_get(url, token, retries=5):
for attempt in range(retries):
r = httpx.get(url, headers={"Authorization": f"Bearer {token}"})
if r.status_code == 200:
return r.json()
if r.status_code == 403:
body = r.json().get("message", "")
if "secondary rate limit" in body.lower():
wait = int(r.headers.get("retry-after", 60))
print(f"Secondary rate limit hit, sleeping {wait}s")
time.sleep(wait)
continue
else:
raise PermissionError(f"Permission denied: {body}")
r.raise_for_status()
raise RuntimeError("Max retries exceeded")
HTTP 429 Too Many Requests (Primary Rate Limit)
Authenticated REST API requests are capped at 5,000 requests per hour per user. GitHub Apps acting as an installation receive up to 15,000 per hour per organization. Unauthenticated requests are limited to 60 per hour per IP.
Exact error message:
{"message": "API rate limit exceeded for user ID 12345.", "documentation_url": "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api"}
Check remaining quota before you hit zero:
curl -sI -H "Authorization: Bearer $TOKEN" https://api.github.com/rate_limit \
| grep x-ratelimit
# x-ratelimit-limit: 5000
# x-ratelimit-remaining: 47
# x-ratelimit-reset: 1708694400 <-- Unix timestamp
Convert the reset timestamp:
date -d @1708694400
Fix — Conditional requests with ETags to conserve quota:
For resources that rarely change (repository metadata, user profiles), cache the ETag header and send If-None-Match on subsequent requests. A 304 Not Modified response costs zero against your rate limit.
# First request — capture ETag
ETAG=$(curl -sI -H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/octocat/hello-world \
| grep -i etag | awk '{print $2}' | tr -d '\r')
# Subsequent request — free if unchanged
curl -si -H "Authorization: Bearer $TOKEN" \
-H "If-None-Match: $ETAG" \
https://api.github.com/repos/octocat/hello-world \
| head -1
# HTTP/2 304 <-- no quota consumed
HTTP 502 Bad Gateway
{"message": "Server Error"}
or an empty response body with status 502.
502s from GitHub are almost always transient infrastructure errors, but they are also reliably triggered by:
- GraphQL queries exceeding the 5,000-node complexity limit
- REST requests that would return payloads over ~1MB (e.g., listing all events on a very active repository without pagination)
- Burst of parallel connections during a GitHub service incident
Diagnostic — Check GitHub status first:
curl -s https://www.githubstatus.com/api/v2/status.json | python3 -m json.tool | grep -A2 status
Fix — Paginate aggressively and add retry logic:
# Always paginate; never request more than 100 items per page
curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.github.com/repos/OWNER/REPO/issues?per_page=100&page=1"
Connection Timeouts
Timeouts manifest as curl: (28) Operation timed out or equivalent library exceptions (requests.exceptions.ReadTimeout, net/http: request canceled).
Common causes:
- Client-side timeout set too low for large Git data (tarball/blob endpoints)
- Network path issue between CI runner and
api.github.com - GitHub API response streaming stalled (rare, retry resolves it)
Fix — Increase timeout and verify DNS:
# Test DNS resolution time
time nslookup api.github.com
# Test with explicit timeout (30s)
curl --max-time 30 -H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/OWNER/REPO/tarball/main -o /dev/null -s -w "%{http_code}\n"
For SDK users (Python requests library):
import requests
r = requests.get(
"https://api.github.com/repos/OWNER/REPO/contents/",
headers={"Authorization": f"Bearer {token}"},
timeout=(5, 30) # (connect_timeout, read_timeout) in seconds
)
Production-Grade Retry Wrapper
For any service making sustained GitHub API calls, a unified retry layer handles all the error codes above:
import time, math, httpx
def github_request(method, url, token, **kwargs):
headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"}
for attempt in range(6):
try:
r = httpx.request(method, url, headers=headers, timeout=30, **kwargs)
except httpx.TimeoutException:
wait = 2 ** attempt
print(f"Timeout, retry {attempt+1} in {wait}s")
time.sleep(wait)
continue
if r.status_code in (200, 201, 204, 304):
return r
if r.status_code == 401:
raise ValueError("GitHub token invalid or expired — rotate credentials")
if r.status_code == 403:
msg = r.json().get("message", "")
if "secondary rate limit" in msg.lower():
wait = int(r.headers.get("retry-after", 60))
else:
raise PermissionError(msg)
time.sleep(wait)
continue
if r.status_code == 429:
reset = int(r.headers.get("x-ratelimit-reset", time.time() + 60))
wait = max(reset - time.time(), 1)
print(f"Primary rate limit, sleeping until reset ({wait:.0f}s)")
time.sleep(wait)
continue
if r.status_code == 502:
wait = 2 ** attempt
print(f"502 Bad Gateway, retry {attempt+1} in {wait}s")
time.sleep(wait)
continue
r.raise_for_status()
raise RuntimeError(f"All retries exhausted for {url}")
Frequently Asked Questions
#!/usr/bin/env bash
# GitHub API Diagnostic Script
# Usage: TOKEN=ghp_xxx bash github_api_diag.sh [owner/repo]
set -euo pipefail
API="https://api.github.com"
REPO="${1:-octocat/hello-world}"
if [[ -z "${TOKEN:-}" ]]; then
echo "ERROR: Set TOKEN environment variable"
exit 1
fi
echo "=== 1. Validate token ==="
USER_RESP=$(curl -sf -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
"$API/user" 2>&1) && echo "Token valid — authenticated as: $(echo $USER_RESP | python3 -c 'import sys,json; print(json.load(sys.stdin)["login"])')" \
|| echo "FAIL: $USER_RESP"
echo ""
echo "=== 2. Check rate limit quota ==="
curl -sI -H "Authorization: Bearer $TOKEN" \
"$API/rate_limit" | grep -i x-ratelimit
echo ""
echo "=== 3. Check x-ratelimit-reset in human time ==="
RESET=$(curl -sI -H "Authorization: Bearer $TOKEN" "$API/rate_limit" \
| grep -i x-ratelimit-reset | awk '{print $2}' | tr -d '\r')
[[ -n "$RESET" ]] && echo "Rate limit resets at: $(date -d @$RESET 2>/dev/null || date -r $RESET)" || echo "Could not parse reset time"
echo ""
echo "=== 4. Check token scopes ==="
curl -sI -H "Authorization: Bearer $TOKEN" "$API/user" \
| grep -i 'x-oauth-scopes\|x-accepted-oauth-scopes'
echo ""
echo "=== 5. Test conditional request (ETag caching) ==="
ETAG=$(curl -sI -H "Authorization: Bearer $TOKEN" \
"$API/repos/$REPO" \
| grep -i '^etag' | awk '{print $2}' | tr -d '\r')
if [[ -n "$ETAG" ]]; then
STATUS=$(curl -so /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
-H "If-None-Match: $ETAG" \
"$API/repos/$REPO")
echo "Conditional request returned: $STATUS (304 = free, no quota used)"
else
echo "WARN: No ETag returned for $API/repos/$REPO"
fi
echo ""
echo "=== 6. Check GitHub status ==="
STATUS=$(curl -s https://www.githubstatus.com/api/v2/status.json \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["status"]["description"])')
echo "GitHub status: $STATUS"
echo ""
echo "=== 7. Test timeout sensitivity ==="
TIME=$(curl -so /dev/null -w "%{time_total}" --max-time 10 \
-H "Authorization: Bearer $TOKEN" \
"$API/repos/$REPO" 2>&1)
echo "Response time: ${TIME}s (warn if >5s)"
echo ""
echo "=== Diagnostic complete ==="Error Medic Editorial
The Error Medic Editorial team consists of senior DevOps engineers, SREs, and platform engineers with combined experience across GitHub Actions, GitLab CI, and large-scale API integrations. We test every command and code snippet against live APIs before publishing.
Sources
- https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- https://docs.github.com/en/rest/using-the-rest-api/troubleshooting-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/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api
- https://stackoverflow.com/questions/22188341/github-api-rate-limit-how-to-avoid-reaching-the-limit
- https://docs.github.com/en/rest/using-the-rest-api/best-practices-for-using-the-rest-api