GitHub API rate limits are the first wall you hit when you try to do anything useful at scale with the GitHub API. The limits are tiered, context-dependent, and documented in ways that require reading between the lines to understand in practice. This guide covers every limit you will encounter in 2026 and gives you practical code to handle each one.
The Two Types of Rate Limits
GitHub enforces two fundamentally different rate limit systems: primary rate limits (total requests per hour) and secondary rate limits (concurrency and burst behavior). Hitting primary limits gives you a clean 403 or 429 with a Retry-After header. Secondary limits are less predictable — they can return 403 errors with an abuse detection message, and GitHub may temporarily block your IP or token without warning.
REST API Rate Limits
- Unauthenticated: 60 requests/hour, per IP address
- Authenticated (personal access token or OAuth): 5,000 requests/hour, per token
- GitHub App tokens: 5,000 requests/hour + 50 requests/hour per installed repo (up to 12,500)
- GITHUB_TOKEN in Actions: 1,000 requests/hour per repository
- Search API (authenticated): 30 requests/minute, 10 for unauthenticated
- Code Search API: 10 requests/minute (separate quota from other search)
GraphQL API Rate Limits
The GraphQL API uses a point-based rate limit system instead of per-request counting. Each authenticated request costs a minimum of 1 point, but complex queries that return many nodes cost more. The limit is 5,000 points/hour for authenticated requests. You can check your remaining points:
# Check your current rate limit status
query {
rateLimit {
limit
remaining
resetAt
used
nodeCount
}
}The GraphQL API is often more efficient for bulk operations because you can fetch more data per request. Instead of making 100 REST calls to get 100 user profiles, you can batch them in a single GraphQL query using aliases or the nodes query:
# Fetch up to 100 user profiles in a single GraphQL request
query GetUsers($ids: [ID!]!) {
nodes(ids: $ids) {
... on User {
login
name
email
company
location
followers {
totalCount
}
repositories(first: 5, orderBy: {field: STARGAZERS, direction: DESC}) {
nodes {
name
stargazerCount
primaryLanguage {
name
}
}
}
}
}
}Secondary Rate Limits (Abuse Detection)
Secondary rate limits trigger when GitHub detects patterns it considers abusive, independent of your request count. Known triggers include: making more than 80–100 requests per minute (even if under the hourly limit), creating too many resources in a short window, running many concurrent API connections from the same IP, and hitting the same endpoint repeatedly in a tight loop.
When you hit secondary limits, GitHub returns a 403 with a message containing "secondary rate limit" or "abuse detection." The Retry-After header is not always present. Safe defaults: add a delay of 1–2 seconds between requests, never exceed 50 requests/minute even with a valid token, and use jitter (random sleep) to avoid pattern detection.
Checking Your Rate Limit Status
import requests
import time
def check_rate_limit(token):
resp = requests.get(
"https://api.github.com/rate_limit",
headers={"Authorization": f"Bearer {token}"}
)
data = resp.json()
core = data["resources"]["core"]
search = data["resources"]["search"]
print(f"Core: {core['remaining']}/{core['limit']} (resets at {core['reset']})")
print(f"Search: {search['remaining']}/{search['limit']}")
return core["remaining"], core["reset"]
def rate_limited_get(url, token, min_remaining=100):
"""Make a GET request, backing off if approaching rate limit."""
headers = {"Authorization": f"Bearer {token}"}
resp = requests.get(url, headers=headers)
remaining = int(resp.headers.get("X-RateLimit-Remaining", 9999))
reset_at = int(resp.headers.get("X-RateLimit-Reset", 0))
if resp.status_code == 429 or remaining < min_remaining:
sleep_time = max(reset_at - time.time(), 0) + 5
print(f"Rate limit approaching, sleeping {sleep_time:.0f}s")
time.sleep(sleep_time)
if resp.status_code == 403 and "secondary" in resp.text.lower():
print("Secondary rate limit hit, sleeping 60s")
time.sleep(60)
return rate_limited_get(url, token, min_remaining)
return respStrategies for High-Volume GitHub Scraping
1. Rotate Multiple Tokens
Each personal access token has its own 5,000 req/hour quota. With 5 tokens rotated evenly, you get 25,000 requests/hour. Create tokens across multiple GitHub accounts (must be real accounts with activity — GitHub detects throwaway accounts). Use a token pool with round-robin or quota-based selection:
import itertools
import threading
class TokenPool:
def __init__(self, tokens):
self.tokens = list(tokens)
self.lock = threading.Lock()
self.cycle = itertools.cycle(self.tokens)
def get_token(self):
with self.lock:
return next(self.cycle)2. Use the GraphQL API for Bulk Fetches
The GraphQL API lets you batch many user lookups into a single request, dramatically reducing total request count. Fetching 50 user profiles via REST = 50 requests. Via GraphQL nodes query = 1 request. For high-volume enrichment, GraphQL is the right choice.
3. Cache Aggressively
User profiles and repo metadata change infrequently. Cache responses with a 24-hour TTL using Redis or a simple SQLite table. Check your cache before hitting the API. For a list of 10,000 developers being re-enriched monthly, caching reduces API calls by 80–90%.
4. Use Conditional Requests
GitHub supports HTTP conditional requests via ETag and Last-Modified headers. If the resource hasn't changed, GitHub returns a 304 Not Modified and the request does NOT count against your rate limit. Use this for polling repos or user profiles you've already fetched:
def fetch_with_etag(url, token, etag_store: dict):
headers = {"Authorization": f"Bearer {token}"}
if url in etag_store:
headers["If-None-Match"] = etag_store[url]
resp = requests.get(url, headers=headers)
if resp.status_code == 304:
return None # No change, doesn't count against rate limit
if "ETag" in resp.headers:
etag_store[url] = resp.headers["ETag"]
return resp.json()Using GitLeads Instead of Raw API Access
If you need developer leads at scale without managing token pools, cache infrastructure, and rate limit logic, GitLeads handles all of this for you. The platform maintains a continuously-updated index of GitHub developer profiles, so you query GitLeads' API (which has no GitHub rate limit exposure on your end) and get enriched leads back in real time. The Starter plan gives you 500 leads/month; Pro gives you 5,000.
Summary Table
- REST unauthenticated: 60 req/hr | Use only for quick tests
- REST authenticated: 5,000 req/hr | Standard for production use
- REST Search: 30 req/min authenticated | Use pagination to maximize
- GraphQL authenticated: 5,000 points/hr | Best for bulk enrichment
- Secondary limits: ~50 req/min burst | Add jitter + delays
- GitHub App: Up to 12,500 req/hr | Best option for highest-volume needs