Working with the GitHub API rate limit #189255
IntroductionBuilding off the great discussion post around GitHub API rate limits by @devopsjester back in June 2025, I wanted to dive a bit a deeper how and methods to work within the GitHub API rate limits. @devopsjester did a great job breaking down the REST API rate limits, the GraphQL API rate limits, secondary rate limits, and overview of importance of the rate limits. To do a quick rehash:
To dive deeper into best practices of GitHub's rate limits, I wanted to provide some recommendations to consider when you build your next awesome GitHub integration. Recommendation 1: Don't use APIs at all, use webhooksThe best advice there is when using the GitHub API is to not use if you do not have to. This may sound a bit counterintuitive in a discussion around GitHub's API, but if a webhook exists it's better to opt for that over an API call. A lot of GitHub integrations are built on extracting data from GitHub and mainly are event driven. Imagine a GitHub integration where when a new PR opens, you want your integration to add feedback or a comment. The common thought is, let's build around the List pull requests REST API endpoint! So, you end up building an integration that looks something like this:
When you start designing, this seems to make sense: everything stays within your codebase and just naturally flows. You just feed a token into for authorization, and with a few lines of code, you will have built a working integration. Truth is, in smaller repositories where there are less frequent changes, this may work well. However, this does not scale and hence why it doesn't follow best practices: as the repository grows with more development work, there will be more PRs. And as the number of PRs grows, there are more API requests to get the specific PRs data that will consume your rate limit. Eventually, you will hit a point where you exhaust your API rate limit and your integration will break. Webhooks on the other hand do not use the API and as it doesn't use the API, no rate limit would ever be hit. Webhooks allow you to subscribe to specific events in your organization or repository. A payload is delivered to your webhook server when the subscribed event occurs. Taking the example above, the flow would look something like this instead: Prerequisite steps:
Steps:
While webhooks do add overhead by having to either set up a webhook proxy service like smee.io or a local webhook listener, this eliminates the need to poll the GitHub LIST API endpoints and allows you to write an app that focuses on just using the GitHub GET API endpoints to retrieve the needed information. Also, once a webhook service is set up, it can be used for other projects or integrations you are building that have webhook capabilities. Recommendation 2: GitHub AppsWhether your integration can use webhooks or not, the next recommendation is to utilize a GitHub App over a service account or For a detailed look at how to build a GitHub App and use the GitHub App's Installation Token for API access, please look at this community discussion post by @loujr. Recommendation 3: Octokit as your libraryOnce you have considered webhooks and GitHub Apps for your GitHub integration, the next will be focusing on what libraries and SDKs you want to build on. The recommendation is to use Octokit as it is the official SDK of GitHub. Not only that, but there are already two plugins built for Most of the bugs and issues seen with adopting rate limit best practices comes with trying to hand build the logic itself. Whether it's adding Other times, developers (understandably) want to build the features for their GitHub integration first as it is more important or more exciting. This often causes rate limit logic to be push into the backlog for later work or a "nice to have". This is where Octokit excels as it already has the two plugins that are built in to handle this logic. The @octokit/plugin-throttling tackles rate limits by queuing requests when you're approaching limits and provides callbacks for both primary and secondary rate limit events. The @octokit/plugin-retry complements @octokit/plugin-throttling by handling transient server errors. Both repositories for the plugins have excellent examples that show how easy it is to implement retries and rate limit throttling so you can focus on developing those features. There are various third party libraries as well but do research and see if the library has rate limit logic and how it handles rate limiting. Recommendation 4: Cache with Conditional RequestsAfter choosing your library and as you begin to build your code around GitHub's APIs, ETags are a great way to monitor the state of an API result if you are using the REST API. Think of ETags like a fingerprint of the resource's current state. Here's the flow:
Here's an example of this using Octokit.js: #!/usr/bin/env node
import { Octokit } from "octokit";
import { readFile, writeFile } from "node:fs/promises";
const OWNER = process.env.GITHUB_OWNER ?? "my-org";
const REPO = process.env.GITHUB_REPO ?? "my-repo";
const TOKEN = process.env.GITHUB_TOKEN;
const ETAG_FILE = `.etag-${OWNER}-${REPO}`;
const CACHE_FILE = `.cache-${OWNER}-${REPO}.json`;
const octokit = new Octokit(TOKEN ? { auth: TOKEN } : {});
async function readIfExists(file) {
try {
return await readFile(file, "utf8");
} catch (err) {
if (err.code === "ENOENT") return null;
throw err;
}
}
let response;
const cachedEtag = (await readIfExists(ETAG_FILE))?.trim();
try {
response = await octokit.request("GET /repos/{owner}/{repo}/pulls", {
owner: OWNER,
repo: REPO,
state: "open",
per_page: 10,
page: 1,
headers: {
accept: "application/vnd.github+json",
"x-github-api-version": "2022-11-28",
...(cachedEtag ? { "if-none-match": cachedEtag } : {}),
},
});
} catch (err) {
if (err.status === 304 || err.response?.status === 304) {
response = { status: 304, data: null, headers: err.response?.headers ?? {} };
} else {
console.error(`HTTP ${err.status ?? "unknown"}`);
process.exit(1);
}
}
console.log(`HTTP status: ${response.status}`);
if (response.status === 304) {
console.log("No changes — serving cached data (0 rate limit cost)");
} else if (response.status === 200) {
if (response.headers.etag) {
await writeFile(ETAG_FILE, `${response.headers.etag}\n`, "utf8");
}
await writeFile(CACHE_FILE, `${JSON.stringify(response.data, null, 2)}\n`, "utf8");
console.log(`New data — ${response.data.length} open PRs`);
}
const cached = await readIfExists(CACHE_FILE);
if (cached === null) {
console.log("Local cache file is missing.");
console.log("[]");
} else {
console.log(cached);
}As there are no official Octokit plugins for ETags, in this example script, we are simply monitoring for While there is no plugin, by including one more header you can monitor for changes while not being billed for it. Consider a case where you scale up this example script to monitor for new Pull Requests for 50 of your repositories every 5 minutes. That means you would be doing the following:
Now let's assume that 90% of the time when you make these requests, there is no change to the Pull Requests. That means 540 requests were made returning that same data, effectively meaning you wasted 540 requests. With ETag caching, you would still do 600 requests but only 60 requests would count against your rate limit while also getting the same data. One other consideration for conditional requests and caching is the A couple things to consider when designing caching strategies:
Wrapping it all upWhile on the surface rate limits seem to be a constraint and a hassle, when respected they can help make your integrations faster, more efficient, and more reliable. GitHub is a huge platform used by the smallest developers to largest companies in the world. The rate limits help maintain a fast and reliable platform for us to build on: whether it is your first python Hello World! to your massive Rust applications. When building your next GitHub integration, consider using webhooks, GitHub Apps, Octokit as your library, and Conditional Requests to help you build a reliable, efficient, and best practice following app! |
Replies: 2 comments
|
As someone learning how to build GitHub integrations, this discussion helped connect several concepts that I had previously treated separately. I knew about webhooks and GitHub Apps individually, but I hadn't considered how combining them with conditional requests and Octokit's throttling plugins creates a much more scalable architecture. The ETag example was particularly helpful because it demonstrates the optimization with real numbers rather than just explaining the concept. Thanks for putting together such a practical guide. |
|
✅ Verified by GitHub
Click to open
IntroductionBuilding off the great discussion post around GitHub API rate limits by @devopsjester back in June 2025, I wanted to dive a bit a deeper how and methods to work within the GitHub API rate limits. @devopsjester did a great job breaking down the REST API rate limits, the GraphQL API rate limits, secondary rate limits, and overview of importance of the rate limits. To do a quick rehash:
To dive deeper into best practices of GitHub's rate limits, I wanted to provide some recommendations to consider when you build your next awesome GitHub integration. Recommendation 1: Don't use APIs at all, use webhooksThe best advice there is when using the GitHub API is to not use if you do not have to. This may sound a bit counterintuitive in a discussion around GitHub's API, but if a webhook exists it's better to opt for that over an API call. A lot of GitHub integrations are built on extracting data from GitHub and mainly are event driven. Imagine a GitHub integration where when a new PR opens, you want your integration to add feedback or a comment. The common thought is, let's build around the List pull requests REST API endpoint! So, you end up building an integration that looks something like this:
When you start designing, this seems to make sense: everything stays within your codebase and just naturally flows. You just feed a token into for authorization, and with a few lines of code, you will have built a working integration. Truth is, in smaller repositories where there are less frequent changes, this may work well. However, this does not scale and hence why it doesn't follow best practices: as the repository grows with more development work, there will be more PRs. And as the number of PRs grows, there are more API requests to get the specific PRs data that will consume your rate limit. Eventually, you will hit a point where you exhaust your API rate limit and your integration will break. Webhooks on the other hand do not use the API and as it doesn't use the API, no rate limit would ever be hit. Webhooks allow you to subscribe to specific events in your organization or repository. A payload is delivered to your webhook server when the subscribed event occurs. Taking the example above, the flow would look something like this instead: Prerequisite steps:
Steps:
While webhooks do add overhead by having to either set up a webhook proxy service like smee.io or a local webhook listener, this eliminates the need to poll the GitHub LIST API endpoints and allows you to write an app that focuses on just using the GitHub GET API endpoints to retrieve the needed information. Also, once a webhook service is set up, it can be used for other projects or integrations you are building that have webhook capabilities. Recommendation 2: GitHub AppsWhether your integration can use webhooks or not, the next recommendation is to utilize a GitHub App over a service account or For a detailed look at how to build a GitHub App and use the GitHub App's Installation Token for API access, please look at this community discussion post by @loujr. Recommendation 3: Octokit as your libraryOnce you have considered webhooks and GitHub Apps for your GitHub integration, the next will be focusing on what libraries and SDKs you want to build on. The recommendation is to use Octokit as it is the official SDK of GitHub. Not only that, but there are already two plugins built for Most of the bugs and issues seen with adopting rate limit best practices comes with trying to hand build the logic itself. Whether it's adding Other times, developers (understandably) want to build the features for their GitHub integration first as it is more important or more exciting. This often causes rate limit logic to be push into the backlog for later work or a "nice to have". This is where Octokit excels as it already has the two plugins that are built in to handle this logic. The @octokit/plugin-throttling tackles rate limits by queuing requests when you're approaching limits and provides callbacks for both primary and secondary rate limit events. The @octokit/plugin-retry complements @octokit/plugin-throttling by handling transient server errors. Both repositories for the plugins have excellent examples that show how easy it is to implement retries and rate limit throttling so you can focus on developing those features. There are various third party libraries as well but do research and see if the library has rate limit logic and how it handles rate limiting. Recommendation 4: Cache with Conditional RequestsAfter choosing your library and as you begin to build your code around GitHub's APIs, ETags are a great way to monitor the state of an API result if you are using the REST API. Think of ETags like a fingerprint of the resource's current state. Here's the flow:
Here's an example of this using Octokit.js: #!/usr/bin/env node
import { Octokit } from "octokit";
import { readFile, writeFile } from "node:fs/promises";
const OWNER = process.env.GITHUB_OWNER ?? "my-org";
const REPO = process.env.GITHUB_REPO ?? "my-repo";
const TOKEN = process.env.GITHUB_TOKEN;
const ETAG_FILE = `.etag-${OWNER}-${REPO}`;
const CACHE_FILE = `.cache-${OWNER}-${REPO}.json`;
const octokit = new Octokit(TOKEN ? { auth: TOKEN } : {});
async function readIfExists(file) {
try {
return await readFile(file, "utf8");
} catch (err) {
if (err.code === "ENOENT") return null;
throw err;
}
}
let response;
const cachedEtag = (await readIfExists(ETAG_FILE))?.trim();
try {
response = await octokit.request("GET /repos/{owner}/{repo}/pulls", {
owner: OWNER,
repo: REPO,
state: "open",
per_page: 10,
page: 1,
headers: {
accept: "application/vnd.github+json",
"x-github-api-version": "2022-11-28",
...(cachedEtag ? { "if-none-match": cachedEtag } : {}),
},
});
} catch (err) {
if (err.status === 304 || err.response?.status === 304) {
response = { status: 304, data: null, headers: err.response?.headers ?? {} };
} else {
console.error(`HTTP ${err.status ?? "unknown"}`);
process.exit(1);
}
}
console.log(`HTTP status: ${response.status}`);
if (response.status === 304) {
console.log("No changes — serving cached data (0 rate limit cost)");
} else if (response.status === 200) {
if (response.headers.etag) {
await writeFile(ETAG_FILE, `${response.headers.etag}\n`, "utf8");
}
await writeFile(CACHE_FILE, `${JSON.stringify(response.data, null, 2)}\n`, "utf8");
console.log(`New data — ${response.data.length} open PRs`);
}
const cached = await readIfExists(CACHE_FILE);
if (cached === null) {
console.log("Local cache file is missing.");
console.log("[]");
} else {
console.log(cached);
}As there are no official Octokit plugins for ETags, in this example script, we are simply monitoring for While there is no plugin, by including one more header you can monitor for changes while not being billed for it. Consider a case where you scale up this example script to monitor for new Pull Requests for 50 of your repositories every 5 minutes. That means you would be doing the following:
Now let's assume that 90% of the time when you make these requests, there is no change to the Pull Requests. That means 540 requests were made returning that same data, effectively meaning you wasted 540 requests. With ETag caching, you would still do 600 requests but only 60 requests would count against your rate limit while also getting the same data. One other consideration for conditional requests and caching is the A couple things to consider when designing caching strategies:
Wrapping it all upWhile on the surface rate limits seem to be a constraint and a hassle, when respected they can help make your integrations faster, more efficient, and more reliable. GitHub is a huge platform used by the smallest developers to largest companies in the world. The rate limits help maintain a fast and reliable platform for us to build on: whether it is your first python Hello World! to your massive Rust applications. When building your next GitHub integration, consider using webhooks, GitHub Apps, Octokit as your library, and Conditional Requests to help you build a reliable, efficient, and best practice following app! Details |

✅ Verified by GitHub
Click to open
Introduction
Building off the great discussion post around GitHub API rate limits by @devopsjester back in June 2025, I wanted to dive a bit a deeper how and methods to work within the GitHub API rate limits. @devopsjester did a great job breaking down the REST API rate limits, the GraphQL API rate limits, secondary rate limits, and overv…