GitHub API rate limit exceeded: primary and secondary limits, REST and GraphQL

By Sazabi Team

Failure facts checked on the vendor’s docs on Nov 8, 2026.

GitHub has at least four ways to say no: a primary limit and a secondary limit, each for REST and for GraphQL. They have different budgets, different responses, and only one of them tells you how much is left.

Primary limits

The primary limit is an hourly budget tied to the credential you use.

CredentialRESTGraphQL
No authentication (by IP address)60 requests an hourNot covered
User, via personal access token or app acting for a user5,000 requests an hour5,000 points an hour
GitHub App installation5,000 an hour, plus 50 for each repository or user past 20, capped at 12,500Same scale and cap, in points
GITHUB_TOKEN in Actions1,000 an hour per repository1,000 points an hour per repository

Enterprise Cloud organizations get higher figures for apps they own. Search endpoints have tighter limits of their own, and Git LFS has a separate bucket. GraphQL points depend on the query: you can ask for rateLimit { cost remaining resetAt } inside a query to see what it cost.

Every response carries x-ratelimit-limit, -remaining, -used, -reset (UTC epoch seconds) and x-ratelimit-resource. That last header names the bucket the call counted against, which is how you tell search or GraphQL use from core use. GitHub warns that remaining counts can differ between responses, because requests are handled in several regions, so pace on the trend and not on an exact number.

Secondary limits

Secondary limits catch behavior, not volume. GitHub lists them: no more than 100 concurrent requests across REST and GraphQL together, 900 points a minute for REST endpoints and 2,000 for GraphQL, 90 seconds of CPU time per 60 seconds of real time, about 80 content-creating requests a minute and 500 an hour, and 2,000 OAuth token requests an hour. The docs add that these can change without notice and you may hit one for undisclosed reasons.

There is no header that shows your secondary budget, and GitHub says you cannot check it. Detection has to come from the failures.

Reading the response

  • REST primary limit: 403 or 429, with x-ratelimit-remaining: 0. Wait for x-ratelimit-reset.
  • REST secondary limit: 403 or 429 with a message naming a secondary rate limit. Wait for retry-after if present, otherwise x-ratelimit-reset if remaining is 0, otherwise at least a minute, then back off exponentially.
  • GraphQL primary limit: HTTP 200, an error in the body, x-ratelimit-remaining: 0.
  • GraphQL secondary limit: 200 or 403 with a secondary-limit message.

The GraphQL cases mean a client that only checks the status code reports success while returning nothing. Log whether the body has an errors array.

GitHub also warns that continuing to send requests while limited can get an integration banned, so a retry loop that ignores these rules is a risk beyond wasted calls.

// One log line per GitHub call. Works for REST and GraphQL.
async function ghFetch(url: string, init: RequestInit & { graphql?: boolean }) {
  const res = await fetch(url, init)
  const body = init.graphql
    ? await res
        .clone()
        .json()
        .catch(() => null)
    : null
  const msg: string = body?.errors?.[0]?.message ?? ''
  console.log(
    JSON.stringify({
      msg: 'vendor call',
      vendor: 'github',
      url,
      status: res.status,
      resource: res.headers.get('x-ratelimit-resource'),
      remaining: Number(res.headers.get('x-ratelimit-remaining')),
      limit: Number(res.headers.get('x-ratelimit-limit')),
      reset: res.headers.get('x-ratelimit-reset'),
      retry_after: res.headers.get('retry-after'),
      graphql_error: msg.toLowerCase().includes('rate limit') ? msg : null,
    })
  )
  return res
}

What to alert on

  • Remaining under 10% of the limit for a given resource. Primary exhaustion is predictable, so alert before it.
  • Any 403 or 429 whose body mentions a rate limit, and any GraphQL 200 with a rate limit error.
  • Concurrency. If you run parallel workers, log in-flight request counts, because the 100 concurrent request cap is secondary and invisible.
  • Writes per minute from automation that creates issues, comments or pull requests.
  • Retries per call, since a loop that ignores retry-after extends the outage.

Example: a backfill that tripped a secondary limit

This is an example, not output from a real project. A made-up repo-sync worker comments on 300 pull requests with 20 parallel workers.

18:20:04Z msg="vendor call" vendor=github resource=core status=201 remaining=3902 retry_after=
18:20:09Z msg="vendor call" vendor=github resource=core status=403 remaining=3871 retry_after=60 graphql_error=
18:20:09Z msg="vendor call" vendor=github resource=core status=403 remaining=3871 retry_after=60 graphql_error=

The primary budget is nearly full, so the hourly limit is not the cause. The 403 comes with retry-after, and the burst of writes points to the secondary limit on content creation. Cutting to one worker with a pause between writes follows the docs' advice.

Checklist

  1. Log x-ratelimit-resource, remaining, reset and retry-after on every call.
  2. Check GraphQL bodies for errors even on 200.
  3. Prefer webhooks to polling, and use conditional requests, as GitHub's best practices page advises.
  4. Pause at least a second between write requests and avoid concurrency.
  5. Authenticate every request.
  6. Do not hammer a 404 or other repeated 4xx: GitHub says it wastes budget and can trigger a secondary limit.

Related: third-party dependency monitoring, vendor degradation and the GitHub integration. If these calls are logged in Sazabi, its agent can watch them.

GitHub rate limits facts

  • Primary REST limits by credential: REST allows 60 requests an hour unauthenticated, 5,000 for an authenticated user, and 1,000 per repository for GITHUB_TOKEN in Actions. (GitHub docs, accessed Nov 8, 2026)
  • GraphQL has its own point budget: GraphQL charges points per query against a separate primary limit, 5,000 points an hour for a user, and each response carries x-ratelimit headers. (GitHub docs, accessed Nov 8, 2026)
  • GraphQL primary limit can return 200: When a GraphQL query exceeds the primary limit, the HTTP status is still 200, with an error in the body and x-ratelimit-remaining of 0. (GitHub docs, accessed Nov 8, 2026)
  • Secondary limits cannot be queried: Secondary limits include 100 concurrent requests and 80 content-creating requests a minute, and GitHub says there is no way to check their status. (GitHub docs, accessed Nov 8, 2026)
  • Backoff rule for secondary limits: Honor retry-after, or x-ratelimit-reset when remaining is 0, or wait at least a minute and then back off exponentially. (GitHub docs, accessed Nov 8, 2026)

With Sazabi

The Sazabi agent automatically sets up sensors for your project and maintains them over time. When your code calls GitHub, it watches a question like this one:

Tell me when GitHub API calls start returning rate limit errors, or when remaining requests drop under 10% of the hourly limit, per token.

When the answer turns abnormal, Sazabi investigates and opens an issue. Vendor rate limiting covers the signal behind it.

Frequently Asked Questions

What is the GitHub API rate limit?

For REST, 60 requests an hour unauthenticated and 5,000 an hour for an authenticated user, with higher figures for some Enterprise Cloud apps. GitHub Actions GITHUB_TOKEN gets 1,000 an hour per repository. GraphQL uses points instead of requests, also 5,000 an hour for a user.

What is the difference between primary and secondary limits?

The primary limit is an hourly budget you can read from x-ratelimit headers. Secondary limits guard against bursts, such as too many concurrent requests or too much content created too fast, and GitHub provides no way to check their status.

Why did I get a 200 when I was rate limited?

GitHub says a GraphQL query that exceeds the primary limit still returns HTTP 200, with an error message and x-ratelimit-remaining at 0. Check the response body for errors, not only the status code.

How long should I wait after a secondary limit?

Use retry-after if it is present. If x-ratelimit-remaining is 0, wait until x-ratelimit-reset. Otherwise wait at least one minute, then lengthen the delay on each failure and stop after a set number of tries.

Sources

  1. Primary limits per credential type, secondary limits, x-ratelimit headers, 403 and 429 responses and retry guidance (accessed Nov 8, 2026)
  2. GraphQL points, the 200 response on a primary limit, the node limit and resource limits (accessed Nov 8, 2026)
  3. Prefer webhooks to polling, use conditional requests, and do not ignore repeated 4xx errors (accessed Nov 8, 2026)