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.
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.
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.
No Token
A very small hourly allowance, tracked by IP address — enough for quick manual testing, not for any real application.
Personal Access Token / OAuth
A substantially larger hourly allowance tied to the authenticated account, suitable for personal scripts and small tools.
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.
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.
| Header | What It Tells You |
|---|---|
| Rate limit total | The full size of your allowance for this hour |
| Rate limit remaining | How many calls you have left before you’d be blocked |
| Rate limit reset | The exact time the counter resets back to full |
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.
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
| Aspect | Primary Rate Limit | Secondary Rate Limit |
|---|---|---|
| What it measures | Total requests within a rolling hour | Request pattern — bursts, concurrency, repetition |
| Visibility | Exact numbers shown in response headers | Not precisely published — behavior-based |
| Typical trigger | Simply exceeding your hourly allowance | Sudden bursts or many concurrent calls |
| How to recover | Wait until the published reset time | Back 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
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.
7Strategy One: Conditional Requests
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.
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.
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.
| Aspect | REST API | GraphQL API |
|---|---|---|
| Cost unit | One request = one unit against primary limit | Point cost based on query complexity |
| Fetching related data | Often needs several separate calls | Often achievable in a single call |
| Best for | Simple, single-resource operations | Complex reads spanning multiple related resources |
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
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.
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.
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.
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.
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.
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.
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
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”
15Frequently Asked Questions
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.
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.
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.
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.
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.