How to fix API rate limit exceeded in Git

GitINTERMEDIATEMEDIUMUpdated May 28, 2026· 1,900 views

GitHub blocks requests past the hourly cap (60 unauthenticated, 5000 authenticated). Fix by sending a token via the Authorization header and honoring Retry-After.

Quick fix

Fixes 403

# 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'
  1. Check your current rate limit status
  2. Authenticate your API requests
  3. Monitor rate limit headers in your code
  4. Implement exponential backoff for rate limit errors
git — error
# 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'
API rate limit exceeded

What this error means

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

Typical symptoms

  • API requests return 403 Forbidden with 'rate limit exceeded' message
  • GitHub API returns 429 Too Many Requests status code
  • Tools or scripts suddenly stop working after many successful requests
  • Error message mentions 'Authenticated requests get a higher rate limit'
  • X-RateLimit-Remaining header shows 0 in API responses
  • GitHub Actions workflows fail with rate limit errors
  • IDE plugins or extensions stop showing GitHub data
  • CI/CD pipelines fail when making multiple GitHub API calls

Common causes

  1. Making API requests without authentication (limited to 60/hour)
  2. Exceeding 5,000 authenticated requests within one hour
  3. Multiple applications sharing the same IP address (for unauthenticated requests)
  4. Polling the API too frequently without caching responses
  5. Scripts or bots making excessive API calls
  6. CI/CD pipelines with many parallel jobs hitting the API
  7. Not implementing proper rate limit handling in code
  8. Using GITHUB_TOKEN which has lower limits (1,000/hour/repo)
  9. Secondary rate limits triggered by concurrent requests or CPU-intensive operations

How to fix "API rate limit exceeded"

1Check your current rate limit status

Before fixing the issue, understand where you stand with your rate limits. You can check this without consuming your quota.

Using curl:

bash
# 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:

json
{
  "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:

bash
# On macOS
date -r 1699999999

# On Linux
date -d @1699999999
2Authenticate your API requests

The 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:

bash
curl -H "Authorization: Bearer ghp_xxxxxxxxxxxx" \
  https://api.github.com/user/repos

Python (requests):

python
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):

javascript
const response = await fetch("https://api.github.com/user/repos", {
  headers: {
    "Authorization": "Bearer ghp_xxxxxxxxxxxx",
    "Accept": "application/vnd.github+json"
  }
});

Using environment variables (recommended):

bash
export GITHUB_TOKEN=ghp_xxxxxxxxxxxx

curl -H "Authorization: Bearer $GITHUB_TOKEN" \
  https://api.github.com/user/repos
3Monitor rate limit headers in your code

Implement 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:

python
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 response

JavaScript implementation:

javascript
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;
}
4Implement exponential backoff for rate limit errors

When you hit a rate limit, implement proper retry logic with exponential backoff instead of immediately retrying.

Python with retry logic:

python
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 retries

JavaScript with retry:

javascript
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");
}
5Use conditional requests with ETags

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:

python
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")
6Switch to GraphQL for efficient data fetching

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):

bash
# 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 requests

GraphQL (single request):

bash
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:

python
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).

7Implement caching to reduce API calls

Cache API responses locally to avoid redundant requests. This is especially important for data that doesn't change frequently.

Simple in-memory cache (Python):

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:

python
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 None

File-based cache for CLI tools:

bash
# 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"
8Handle rate limits in GitHub Actions

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:

yaml
- 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:

yaml
- name: API call with PAT
  env:
    GH_TOKEN: ${{ secrets.PAT_TOKEN }}  # 5,000/hour limit
  run: |
    gh api /repos/owner/repo/issues

Implement retry in workflow:

yaml
- 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 }}/issues

Reduce API calls in matrix jobs:

yaml
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 .

Advanced notes

### 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:

bash
# 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

bash
# 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
// 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
# 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

bash
# 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 | jq

Frequently asked questions

What does "403" mean?
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.
How do I fix "403"?
GitHub blocks requests past the hourly cap (60 unauthenticated, 5000 authenticated). Fix by sending a token via the Authorization header and honoring Retry-After.
What causes "403"?
Making API requests without authentication (limited to 60/hour) Exceeding 5,000 authenticated requests within one hour Multiple applications sharing the same IP address (for unauthenticated requests)
Is the "403" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading