Error Medic

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.

Last updated:
Last verified:
2,102 words
Key Takeaways
  • 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.
GitHub API Error Fix Approaches Compared
MethodWhen to UseTime to ImplementRisk
Add/rotate PAT token401 Unauthorized or missing token5 minutesLow — standard credential rotation
Check token scopes403 on a specific resource (repo, org)5 minutesLow — read-only settings change
Respect Retry-After header429 secondary rate limit or 5031–2 hoursLow — purely additive logic
Exponential back-off with jitterAny transient 5xx or 4292–4 hoursLow — standard resilience pattern
ETags and conditional requestsRepeated reads of the same resource2–4 hoursLow — reduces quota consumption
GitHub App with multiple installationsNeed >5,000 req/hour sustained1–2 daysMedium — requires App registration
GraphQL batchingMany small REST calls4–8 hoursMedium — query language change
Self-hosted GitHub EnterpriseUnlimited API calls behind firewallDays–weeksHigh — 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-limited
  • x-ratelimit-reset: Unix timestamp when the window resets
  • x-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

  1. Regenerate your PAT at https://github.com/settings/tokens
  2. Ensure fine-grained PAT has the correct resource permissions (Contents: Read, Metadata: Read, etc.)
  3. Strip any whitespace: export GITHUB_TOKEN=$(echo -n "$GITHUB_TOKEN" | tr -d '\n')
  4. 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:org for 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

  1. Create a GitHub App instead of using a PAT
  2. Install it on multiple organizations or user accounts
  3. Each installation gets its own 5,000 req/hour quota
  4. 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

  1. Monitor your quota proactively: poll /rate_limit and emit metrics to Prometheus or Datadog
  2. Use GraphQL for batch reads: one GraphQL request can fetch data that would require dozens of REST calls
  3. Paginate efficiently: always use per_page=100 and use cursor-based pagination for GraphQL
  4. Cache aggressively: store responses with their ETag; re-validate before each use
  5. Separate token pools by function: use one PAT for CI, another for bots, another for developer tooling to avoid contention
  6. Set request timeouts: always set a socket timeout (30s recommended) to avoid hanging goroutines/threads that consume connection slots

Frequently Asked Questions

bash
#!/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. ==="
E

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

Related Articles in Github Api

Explore More API Errors Guides