API Rate Limiting Strategies in GitHub

API Rate Limiting Strategies in GitHub

How GitHub keeps its API fair and stable for millions of callers at once — and the practical strategies engineers use to build reliable tools that never get shut out.

Imagine a single water tap shared by an entire apartment building. If one resident left it running all day to fill a swimming pool, everyone else on that floor would be left with a trickle. To keep things fair, the building installs a simple rule: each apartment gets a fixed amount of water per hour, tracked automatically, with a gentle reminder posted on the meter showing exactly how much is left. GitHub’s API works under the same philosophy — every application calling it shares the same infrastructure as millions of others, so GitHub hands out a metered “allowance” of requests, tells you exactly how much you have left after every call, and expects well-behaved callers to plan around it instead of hammering the tap.

1What Is API Rate Limiting?

A foundational idea before we look at GitHub’s specific version of it.

The basic idea

Rate limiting is a rule that caps how many requests a caller can make to a service within a given time window. Once that cap is hit, the service stops answering that caller’s requests — usually temporarily — until the window resets. It exists to protect the service’s servers from being overwhelmed by any single user, whether that user is being careless or genuinely malicious.

Simple Analogy

Think of a library that lets each visitor borrow at most 10 books at a time. It’s not that the library dislikes readers — it’s that without a limit, one enthusiastic visitor could walk out with the entire shelf, leaving nothing for anyone else.

i
Key Idea

Rate limits are not a punishment aimed at any one developer — they’re a shared-resource fairness mechanism that protects the platform for every single caller at once, including the one hitting the limit.

2Primary Rate Limits: The Predictable Allowance

GitHub’s most well-known limit — the number most developers already have some intuition for.

The primary rate limit is a straightforward hourly counter: how a request is authenticated determines how large that counter is. An unauthenticated request (no token at all) gets a very small allowance, since GitHub has no way to distinguish one anonymous caller from another. An authenticated request tied to a personal account gets a much larger allowance, and requests made through a registered GitHub App can receive an even higher allowance that scales with the number of repositories or organizations the app is installed on.

Unauthenticated

No Token

A very small hourly allowance, tracked by IP address — enough for quick manual testing, not for any real application.

Authenticated (User)

Personal Access Token / OAuth

A substantially larger hourly allowance tied to the authenticated account, suitable for personal scripts and small tools.

GitHub App

Installation Token

An allowance that scales upward as the app is installed on more repositories, designed for tools meant to operate at real organizational scale.

Enterprise

GitHub Enterprise Cloud/Server

Enterprise customers can receive elevated limits appropriate for internal tooling that touches thousands of repositories.

3Reading Your Remaining Allowance

GitHub doesn’t make you guess how close you are to the limit — it tells you on every single response.

Every API response includes a small set of headers describing exactly where the caller stands: how large the total allowance is, how many requests remain in the current window, and the exact moment the window resets. A well-built integration reads these headers on every call rather than waiting to be told “no” — the same way a careful driver watches the fuel gauge instead of waiting for the engine to actually stop.

HeaderWhat It Tells You
Rate limit totalThe full size of your allowance for this hour
Rate limit remainingHow many calls you have left before you’d be blocked
Rate limit resetThe exact time the counter resets back to full
!
Common Mistake

Ignoring these headers until a request actually fails. By the time you hit zero remaining, you’ve already lost the ability to make any more calls until the reset time — reading the headers proactively lets you slow down gracefully instead.

4Secondary Rate Limits: The Hidden Guard Rail

Staying under your hourly total doesn’t automatically mean you’re safe.

Beyond the simple hourly counter, GitHub also watches the pattern of how requests arrive — this is the secondary rate limit, and it exists to catch abusive behavior that a simple counter would miss entirely. A caller could technically stay under their hourly total while still firing dozens of requests within the same single second, or repeatedly hammering one specific, expensive endpoint — both of which can strain GitHub’s infrastructure even if the raw hourly number looks fine.

Concurrent request limits

Sending too many requests to the API at the exact same time, even briefly, can trigger a secondary limit regardless of your remaining hourly allowance.

Rapid bursts

Sending a large number of requests in a very short window — even if it would still fit inside the hourly total — looks like automated hammering and can be flagged.

Repeated content-creating requests

Rapidly creating many issues, comments, or pull requests in quick succession is treated with extra caution, since this pattern resembles spam behavior.

Simple Analogy

The primary limit is like a monthly data cap on your phone plan. The secondary limit is more like your provider noticing you’re suddenly uploading gigabytes every second, even if you’re nowhere near your monthly cap — that sudden spike pattern gets flagged on its own.

5Primary vs. Secondary Limits at a Glance

AspectPrimary Rate LimitSecondary Rate Limit
What it measuresTotal requests within a rolling hourRequest pattern — bursts, concurrency, repetition
VisibilityExact numbers shown in response headersNot precisely published — behavior-based
Typical triggerSimply exceeding your hourly allowanceSudden bursts or many concurrent calls
How to recoverWait until the published reset timeBack off using the Retry-After guidance and slow down

6The Request Lifecycle Under Rate Limiting

sequenceDiagram
    participant App as Client Application
    participant GH as GitHub API

    App->>GH: Request #1
    GH-->>App: 200 OK + remaining count
    App->>GH: Request #2 ... #N (burst)
    GH-->>App: 403/429 + Retry-After header
    App->>App: Pause for Retry-After duration
    App->>GH: Retry request
    GH-->>App: 200 OK, allowance restored
        
FIG 1 — A well-behaved client reads the remaining count on every response and honors the Retry-After guidance the moment a limit is hit.

Notice that being rate-limited is not a fatal error — it’s a temporary, expected condition with a clearly published recovery time. Treating it as a normal part of the request lifecycle, rather than an unexpected crash, is the mental shift that separates a fragile integration from a resilient one.

The single most effective way to reduce your rate limit consumption without losing any data freshness.

Many GitHub API responses include a validation tag that represents the exact state of that data at the moment it was fetched. On the next request for the same resource, the caller can send that tag back and ask GitHub “has this changed since I last checked?” If nothing has changed, GitHub replies with a lightweight “not modified” response — and critically, that kind of response does not consume any of the caller’s rate limit allowance at all.

Simple Analogy

It’s like calling a friend and asking “did anything change since we last spoke?” instead of asking them to repeat the entire conversation from scratch every single time. If nothing changed, the call is quick and costs almost nothing.

i
Why This Matters

An integration that polls the same handful of resources every few minutes can cut its real rate limit usage dramatically just by adopting conditional requests, since most polls will find nothing new to report.

8Strategy Two: Prefer Webhooks Over Polling

The best request is the one you never had to make.

Polling means repeatedly asking “has anything changed yet?” on a fixed schedule, whether or not the answer is actually yes. Webhooks flip this around entirely: instead of the application asking, GitHub proactively pushes a notification the moment something relevant actually happens — a new pull request opened, a check run completed, an issue commented on. This eliminates the vast majority of “just checking” requests that would otherwise consume rate limit for no benefit.

Polling (Less Efficient)

An integration checks every two minutes whether a pull request has new comments, even during hours when nothing is happening at all — burning allowance on empty answers.

Webhooks (More Efficient)

The same integration instead subscribes to a comment-created event and is notified instantly, the moment it actually happens — zero wasted requests during quiet periods.

9Strategy Three: Choose GraphQL for Complex Reads

GitHub offers two different API styles, and they consume rate limit in very different ways.

The traditional REST API counts each individual call toward the primary limit, regardless of how small or large the response is — fetching a repository, then its issues, then each issue’s comments, could mean many separate calls. GitHub’s GraphQL API instead uses a point-based cost system: a single, carefully shaped GraphQL query can retrieve a repository, its issues, and their comments all in one request, and its “cost” is calculated based on the actual complexity of what was requested rather than the number of separate calls it would otherwise have taken.

AspectREST APIGraphQL API
Cost unitOne request = one unit against primary limitPoint cost based on query complexity
Fetching related dataOften needs several separate callsOften achievable in a single call
Best forSimple, single-resource operationsComplex reads spanning multiple related resources
i
Practical Takeaway

An integration that needs a repository’s details, its recent issues, and each issue’s labels in one screen will typically consume far less allowance fetching that with one well-designed GraphQL query than with the equivalent chain of REST calls.

10Strategy Four: Caching & Smart Pagination

1

Cache aggressively for slow-changing data

Repository metadata, team membership, and organization settings rarely change minute-to-minute — cache them locally and refresh on a sensible schedule instead of re-fetching on every use.

2

Request the largest sensible page size

Fetching results in larger pages means fewer total requests to retrieve the same amount of data, directly reducing rate limit consumption for large listings.

3

Stop paginating once you have what you need

If you only need the five most recent items, don’t walk through every page of a thousand-item list — request just enough and stop.

4

Follow the provided next-page link exactly

Use the pagination link GitHub returns rather than constructing your own page-number guesses, which avoids wasted or malformed requests.

11Strategy Five: Scaling with GitHub Apps

For tools that need to operate across many repositories or organizations, the authentication method itself becomes a scaling strategy.

A single personal access token shares one flat hourly allowance no matter how many repositories your tool touches, which becomes a bottleneck fast at real organizational scale. A GitHub App, by contrast, is installed separately per organization or repository, and each installation gets its own token with its own allowance — meaning the total capacity available to a well-designed App effectively grows alongside its adoption, rather than staying fixed.

Simple Analogy

A personal access token is like one shared cash register for an entire mall. A GitHub App is like giving every single store its own register — the total throughput of the whole mall scales with how many stores exist, instead of everyone queuing at one counter.

12Design Patterns & Anti-patterns

Pattern: Exponential Backoff

When a request fails due to rate limiting, wait a short amount of time before retrying, and double that wait on each subsequent failure — this naturally spaces out retries instead of hammering the API the instant the limit resets.

Pattern: Queue and Throttle Writes

Route write operations (like creating issues or comments) through an internal queue that releases them at a steady, controlled pace, rather than firing them all at once in response to a burst of user activity.

ANTI-PATTERN-01 Avoid
Problem

Polling the same endpoint on a fixed, aggressive interval regardless of whether anything is actually changing.

Why It’s Harmful

It burns through the hourly allowance on requests that almost always return “nothing new,” starving the application of allowance for the requests that actually matter.

Correct Approach

Replace fixed-interval polling with webhooks where possible, and fall back to conditional requests for anything that still needs to be polled.

ANTI-PATTERN-02 Avoid
Problem

Retrying a rate-limited request immediately, in a tight loop, without waiting for the reset time or any backoff.

Why It’s Harmful

This is exactly the burst behavior that triggers secondary rate limits, turning a temporary primary-limit slowdown into a much longer, stricter secondary-limit penalty.

Correct Approach

Always honor the reset time and any retry guidance GitHub provides, and add exponential backoff on top for extra safety.

13Best Practices & Common Mistakes

Best Practices

  • Read the remaining-allowance headers on every response, not just after a failure
  • Use conditional requests wherever the same resource is checked repeatedly
  • Prefer webhooks over polling wherever the API supports the relevant event
  • Use GraphQL for complex, multi-resource reads to reduce total request count
  • Authenticate through a GitHub App when operating across many repositories or organizations

Common Mistakes

  • Treating a rate-limit response as an unexpected crash instead of a normal, expected condition
  • Retrying in a tight loop without any backoff, triggering secondary limits
  • Fetching full pages of data repeatedly for information that rarely changes
  • Relying on a single personal access token for a tool meant to scale across many organizations

14Advantages & Trade-offs of Each Strategy

0
allowance cost for a “not modified” conditional response
1
GraphQL call can replace several chained REST calls
Many
independent allowances via per-installation GitHub App tokens

Advantages of a Deliberate Strategy

  • Predictable, resilient behavior under load instead of sporadic failures
  • Lower real-world latency, since cached and conditional responses return faster
  • Scales naturally as the tool is adopted by more repositories or organizations

Trade-offs to Accept

  • Conditional requests and caching add a small amount of implementation complexity
  • Webhook-based systems need a reliable, publicly reachable endpoint to receive events
  • GraphQL’s cost model requires learning a new way of estimating request “cost”
“The fastest API call is the one you found a way not to make.”

15Frequently Asked Questions

Q1Does checking my remaining rate limit itself use up part of the allowance?

Dedicated rate-limit status checks are designed to be lightweight and are generally not counted against the primary limit the same way an ordinary data-fetching request is.

Q2Why was I rate-limited even though I hadn’t used my full hourly allowance?

That’s almost always the secondary rate limit — it reacts to the pattern of requests, like sudden bursts or high concurrency, independently of how much of your hourly total remains.

Q3Do GitHub Apps and personal access tokens share the same allowance?

No — a GitHub App installation gets its own separate token and allowance per installation, which is exactly why Apps scale better than a single shared personal token for multi-organization tools.

Q4Is GraphQL always cheaper than REST?

Not automatically — a poorly shaped GraphQL query requesting excessive nested data can cost more points than a couple of targeted REST calls. The benefit comes from thoughtfully shaping the query to fetch only what’s needed.

Q5What’s the very first thing to try if my integration keeps getting rate-limited?

Check whether you’re polling on a fixed schedule instead of using webhooks, and whether you’re re-fetching unchanged data instead of using conditional requests — these two changes alone resolve the majority of rate-limiting problems.

16Summary and Key Takeaways

API Rate Limiting on GitHub is a shared-fairness mechanism, not an obstacle to be fought against. A primary hourly allowance keeps overall usage bounded, while a behavior-aware secondary limit catches burst patterns a simple counter would miss. The real skill in building reliable GitHub integrations isn’t avoiding limits entirely — it’s designing around them deliberately, using conditional requests, webhooks, GraphQL, caching, and GitHub Apps to make every single request count.

Key Takeaways

  • Two distinct limits exist — a predictable, hourly primary limit and a behavior-based secondary limit that watches for bursts and high concurrency.
  • Response headers are your dashboard — a well-built integration reads remaining allowance and reset time on every call, not just after a failure.
  • Conditional requests are nearly free — an unchanged resource can be re-checked at essentially zero rate-limit cost.
  • Webhooks beat polling — the most efficient request is the one that’s replaced by GitHub proactively notifying you instead.
  • GraphQL consolidates complex reads — one well-shaped query can replace several chained REST calls.
  • GitHub Apps scale better than personal tokens — each installation carries its own independent allowance.
  • Backoff and pacing prevent secondary limits — spacing out retries and throttling writes keeps an integration well inside the platform’s fairness rules.