GitHub API Rate Limits: Finding Leads at Scale (2026)

GitHub API rate limits explained for lead generation at scale. Learn REST vs GraphQL limits, authenticated vs unauthenticated quotas, and how to find GitHub leads without hitting the ceiling.

Published: April 10, 2026Updated: April 17, 202610 min read

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 resp

Strategies 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

Want more like this? Get the weekly developer lead playbook.

No spam. 5 emails over 2 weeks. Unsubscribe anytime.

Related Articles

How to Find Leads on GitHub: The Complete Guide (2026)
10 min read
GitHub Leads vs LinkedIn Leads: When to Use Which (2026)
9 min read
GDPR Compliance for GitHub Lead Scraping: What You Must Know
8 min read