The 'API rate limit exceeded' error occurs when you've made too many requests to the GitHub API within a given time period. Unauthenticated requests are limited to 60 per hour, while authenticated requests get 5,000 per hour. The fix is to authenticate your requests or implement rate limit handling.
This error indicates that you've exceeded GitHub's API rate limit, which is a protection mechanism to ensure fair usage and prevent abuse of their servers. GitHub enforces different rate limits depending on whether your requests are authenticated or not. **Rate limits by authentication type:** - **Unauthenticated requests**: 60 requests per hour per IP address - **Authenticated requests (Personal Access Token)**: 5,000 requests per hour per user - **GitHub Apps on Enterprise Cloud**: 15,000 requests per hour - **GITHUB_TOKEN in GitHub Actions**: 1,000 requests per hour per repository When you hit the rate limit, GitHub returns a 403 Forbidden or 429 Too Many Requests response. The message helpfully reminds you that authenticated requests receive a much higher limit (83x more than unauthenticated). This error commonly affects: - Applications making API calls without authentication - Scripts that poll the API frequently - CI/CD pipelines with many GitHub API operations - Development tools and IDE plugins that interact with GitHub
Before fixing the issue, understand where you stand with your rate limits. You can check this without consuming your quota.
Using curl:
# Check rate limit (unauthenticated)
curl -s https://api.github.com/rate_limit | jq '.rate'
# Check rate limit (authenticated)
curl -s -H "Authorization: Bearer YOUR_TOKEN" \
https://api.github.com/rate_limit | jq '.rate'Response example:
{
"limit": 5000,
"remaining": 4892,
"reset": 1699999999,
"used": 108,
"resource": "core"
}Understanding the response:
- limit: Maximum requests allowed per hour
- remaining: Requests left in current window
- reset: Unix timestamp when the limit resets
- used: Requests consumed so far
Convert reset time to human-readable:
# On macOS
date -r 1699999999
# On Linux
date -d @1699999999The most effective solution is to authenticate your requests, which increases your limit from 60 to 5,000 requests per hour.
Step 1: Create a Personal Access Token (PAT)
1. Go to [github.com/settings/tokens](https://github.com/settings/tokens)
2. Click Generate new token > Generate new token (classic)
3. Give it a descriptive name (e.g., "API Access")
4. Select scopes based on your needs:
- public_repo - Read public repository data
- repo - Full access to private repositories
- read:user - Read user profile data
5. Click Generate token and copy it immediately
Step 2: Use the token in your requests
curl:
curl -H "Authorization: Bearer ghp_xxxxxxxxxxxx" \
https://api.github.com/user/reposPython (requests):
import requests
headers = {
"Authorization": "Bearer ghp_xxxxxxxxxxxx",
"Accept": "application/vnd.github+json"
}
response = requests.get(
"https://api.github.com/user/repos",
headers=headers
)JavaScript (Node.js):
const response = await fetch("https://api.github.com/user/repos", {
headers: {
"Authorization": "Bearer ghp_xxxxxxxxxxxx",
"Accept": "application/vnd.github+json"
}
});Using environment variables (recommended):
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx
curl -H "Authorization: Bearer $GITHUB_TOKEN" \
https://api.github.com/user/reposImplement rate limit monitoring to prevent hitting limits. GitHub includes rate limit information in every API response.
Key headers to track:
| Header | Description |
|--------|-------------|
| x-ratelimit-limit | Maximum requests allowed |
| x-ratelimit-remaining | Requests left in window |
| x-ratelimit-reset | Unix timestamp for reset |
| x-ratelimit-used | Requests used so far |
Python implementation:
import requests
import time
def make_github_request(url, token):
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
# Check rate limit headers
remaining = int(response.headers.get("X-RateLimit-Remaining", 0))
reset_time = int(response.headers.get("X-RateLimit-Reset", 0))
if remaining < 10:
wait_time = reset_time - time.time()
if wait_time > 0:
print(f"Rate limit low. Waiting {wait_time:.0f} seconds...")
time.sleep(wait_time + 1)
return responseJavaScript implementation:
async function makeGitHubRequest(url, token) {
const response = await fetch(url, {
headers: { "Authorization": `Bearer ${token}` }
});
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining"));
const resetTime = parseInt(response.headers.get("X-RateLimit-Reset"));
if (remaining < 10) {
const waitTime = (resetTime * 1000) - Date.now();
if (waitTime > 0) {
console.log(`Rate limit low. Waiting ${waitTime/1000}s...`);
await new Promise(resolve => setTimeout(resolve, waitTime + 1000));
}
}
return response;
}When you hit a rate limit, implement proper retry logic with exponential backoff instead of immediately retrying.
Python with retry logic:
import requests
import time
def github_request_with_retry(url, token, max_retries=3):
headers = {"Authorization": f"Bearer {token}"}
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response
if response.status_code in (403, 429):
# Check for rate limit
remaining = int(response.headers.get("X-RateLimit-Remaining", 1))
if remaining == 0:
reset_time = int(response.headers.get("X-RateLimit-Reset", 0))
wait_time = max(reset_time - time.time(), 0) + 1
# Also check retry-after header
retry_after = response.headers.get("Retry-After")
if retry_after:
wait_time = max(wait_time, int(retry_after))
print(f"Rate limited. Waiting {wait_time:.0f}s (attempt {attempt + 1})")
time.sleep(wait_time)
continue
# Exponential backoff for other errors
wait_time = (2 ** attempt) * 1
time.sleep(wait_time)
return response # Return last response after all retriesJavaScript with retry:
async function githubRequestWithRetry(url, token, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, {
headers: { "Authorization": `Bearer ${token}` }
});
if (response.ok) return response;
if (response.status === 403 || response.status === 429) {
const remaining = parseInt(response.headers.get("X-RateLimit-Remaining"));
if (remaining === 0) {
const resetTime = parseInt(response.headers.get("X-RateLimit-Reset"));
const retryAfter = response.headers.get("Retry-After");
let waitTime = Math.max((resetTime * 1000) - Date.now(), 0) + 1000;
if (retryAfter) {
waitTime = Math.max(waitTime, parseInt(retryAfter) * 1000);
}
console.log(`Rate limited. Waiting ${waitTime/1000}s`);
await new Promise(r => setTimeout(r, waitTime));
continue;
}
}
// Exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 1000));
}
throw new Error("Max retries exceeded");
}Reduce API calls by using conditional requests. If data hasn't changed, GitHub returns 304 Not Modified without counting against your rate limit.
How it works:
1. Make initial request and save the ETag header
2. On subsequent requests, include If-None-Match header with the ETag
3. If data unchanged, get 304 response (doesn't count toward limit)
Python example:
import requests
class GitHubClient:
def __init__(self, token):
self.token = token
self.etag_cache = {} # Store ETags by URL
self.data_cache = {} # Store responses by URL
def get(self, url):
headers = {"Authorization": f"Bearer {self.token}"}
# Add If-None-Match if we have a cached ETag
if url in self.etag_cache:
headers["If-None-Match"] = self.etag_cache[url]
response = requests.get(url, headers=headers)
if response.status_code == 304:
# Data unchanged, return cached version
print("Using cached data (304 Not Modified)")
return self.data_cache[url]
if response.status_code == 200:
# Cache the new data and ETag
etag = response.headers.get("ETag")
if etag:
self.etag_cache[url] = etag
self.data_cache[url] = response.json()
return response.json()
response.raise_for_status()
# Usage
client = GitHubClient("ghp_xxxxxxxxxxxx")
repos = client.get("https://api.github.com/user/repos")
# Second call returns cached data if unchanged
repos = client.get("https://api.github.com/user/repos")GitHub's GraphQL API lets you fetch exactly what you need in a single request, reducing the number of API calls.
REST vs GraphQL comparison:
REST (multiple requests):
# Get user info (1 request)
GET /users/octocat
# Get user repos (1 request)
GET /users/octocat/repos
# Get each repo's issues (N requests)
GET /repos/octocat/repo1/issues
GET /repos/octocat/repo2/issues
# ... more requestsGraphQL (single request):
curl -X POST https://api.github.com/graphql \
-H "Authorization: Bearer ghp_xxxxxxxxxxxx" \
-d '{
"query": "query { user(login: \"octocat\") { name repositories(first: 10) { nodes { name issues(first: 5) { nodes { title } } } } } }"
}'Python GraphQL example:
import requests
def graphql_query(token, query, variables=None):
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {"query": query}
if variables:
payload["variables"] = variables
response = requests.post(
"https://api.github.com/graphql",
headers=headers,
json=payload
)
return response.json()
# Fetch user and repos in one request
query = """
query($login: String!) {
user(login: $login) {
name
repositories(first: 10, orderBy: {field: UPDATED_AT, direction: DESC}) {
nodes {
name
stargazerCount
issues(first: 5, states: OPEN) {
totalCount
nodes {
title
}
}
}
}
}
}
"""
result = graphql_query(token, query, {"login": "octocat"})Note: GraphQL has its own rate limit of 5,000 points per hour (query complexity determines points used).
Cache API responses locally to avoid redundant requests. This is especially important for data that doesn't change frequently.
Simple in-memory cache (Python):
import time
from functools import lru_cache
@lru_cache(maxsize=100)
def get_cached_user(token, username, ttl_hash=None):
"""Cache user data. ttl_hash changes to invalidate cache."""
import requests
response = requests.get(
f"https://api.github.com/users/{username}",
headers={"Authorization": f"Bearer {token}"}
)
return response.json()
def get_ttl_hash(seconds=300):
"""Return same value within 'seconds' window."""
return round(time.time() / seconds)
# Usage - cached for 5 minutes
user = get_cached_user(token, "octocat", get_ttl_hash(300))Redis cache example:
import redis
import json
import requests
r = redis.Redis(host='localhost', port=6379, db=0)
def get_github_data(url, token, cache_ttl=300):
# Check cache first
cache_key = f"github:{url}"
cached = r.get(cache_key)
if cached:
return json.loads(cached)
# Fetch from API
response = requests.get(url, headers={
"Authorization": f"Bearer {token}"
})
if response.status_code == 200:
data = response.json()
# Cache for 5 minutes
r.setex(cache_key, cache_ttl, json.dumps(data))
return data
return NoneFile-based cache for CLI tools:
# Cache GitHub API responses with curl
CACHE_FILE="/tmp/github_cache_$(echo $URL | md5sum | cut -d' ' -f1)"
CACHE_TTL=300 # 5 minutes
if [ -f "$CACHE_FILE" ]; then
AGE=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE")))
if [ $AGE -lt $CACHE_TTL ]; then
cat "$CACHE_FILE"
exit 0
fi
fi
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" "$URL" | tee "$CACHE_FILE"GitHub Actions has specific rate limit considerations. The GITHUB_TOKEN has a limit of 1,000 requests per hour per repository.
Check rate limit in workflow:
- name: Check rate limit
run: |
curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/rate_limit | jq '.rate'Use a PAT for higher limits:
- name: API call with PAT
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN }} # 5,000/hour limit
run: |
gh api /repos/owner/repo/issuesImplement retry in workflow:
- name: Make API request with retry
uses: nick-fields/retry@v2
with:
timeout_minutes: 5
max_attempts: 3
retry_wait_seconds: 60
command: |
curl -f -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/issuesReduce API calls in matrix jobs:
jobs:
setup:
runs-on: ubuntu-latest
outputs:
data: ${{ steps.fetch.outputs.data }}
steps:
- id: fetch
run: |
DATA=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }})
echo "data=$DATA" >> $GITHUB_OUTPUT
matrix-job:
needs: setup
strategy:
matrix:
item: [1, 2, 3]
steps:
# Use cached data instead of making API calls
- run: echo '${{ needs.setup.outputs.data }}' | jq .### Secondary Rate Limits
Beyond primary rate limits, GitHub enforces secondary limits to prevent abuse:
| Limit Type | Threshold |
|------------|-----------|
| Concurrent requests | 100 maximum |
| REST API points | 900 points/minute |
| CPU time | 90 seconds per 60 real seconds |
| Content creation | 80 requests/minute, 500/hour |
Point values for REST API:
- GET, HEAD requests: 1 point
- POST, PATCH, PUT, DELETE: 5 points
If you trigger secondary limits, you'll receive a 403 response with a message about abuse detection. Wait at least one minute before retrying.
### GitHub Apps for Higher Limits
For applications needing more than 5,000 requests/hour, consider using a GitHub App:
# GitHub Apps can have higher limits based on:
# - Number of repositories
# - Number of organization users
# - Enterprise Cloud status (15,000/hour)### Rate Limit for Search API
The Search API has separate limits:
- Authenticated: 30 requests/minute
- Unauthenticated: 10 requests/minute
# Check search rate limit specifically
curl -H "Authorization: Bearer TOKEN" \
https://api.github.com/rate_limit | jq '.resources.search'### Handling Shared IP Addresses
For unauthenticated requests from shared infrastructure (cloud functions, CI runners):
1. Always authenticate - Each user gets their own quota
2. Use GitHub Apps - Installation tokens are separate from user tokens
3. Implement request queuing - Spread requests over time
### Octokit Libraries
Official GitHub libraries handle rate limiting automatically:
// JavaScript/TypeScript
import { Octokit } from "@octokit/rest";
import { throttling } from "@octokit/plugin-throttling";
const MyOctokit = Octokit.plugin(throttling);
const octokit = new MyOctokit({
auth: "ghp_xxxxxxxxxxxx",
throttle: {
onRateLimit: (retryAfter, options) => {
console.warn(`Rate limited, retrying after ${retryAfter}s`);
return true; // Retry
},
onSecondaryRateLimit: (retryAfter, options) => {
console.warn(`Secondary rate limit, waiting ${retryAfter}s`);
return true;
}
}
});# Python
from github import Github
from github import RateLimitExceededException
import time
g = Github("ghp_xxxxxxxxxxxx")
try:
repos = g.get_user().get_repos()
except RateLimitExceededException:
reset_time = g.get_rate_limit().core.reset
sleep_time = (reset_time - datetime.now()).total_seconds()
time.sleep(sleep_time + 1)### Debugging Rate Limit Issues
# Verbose output to see rate limit headers
curl -v -H "Authorization: Bearer TOKEN" \
https://api.github.com/user 2>&1 | grep -i x-ratelimit
# Check all rate limit categories
curl -s -H "Authorization: Bearer TOKEN" \
https://api.github.com/rate_limit | jqkex_exchange_identification: Connection closed by remote host
Connection closed by remote host when connecting to Git server
fatal: unable to access: Proxy auto-configuration failed
How to fix 'Proxy auto-configuration failed' in Git
fatal: unable to access: Authentication failed (proxy requires basic auth)
How to fix 'Authentication failed (proxy requires basic auth)' in Git
fatal: unable to access: no_proxy configuration not working
How to fix 'no_proxy configuration not working' in Git
fatal: unable to read tree object in treeless clone
How to fix 'unable to read tree object in treeless clone' in Git