GitHub API 速率限制:未经认证的304 仍然费用 你是一个请求

2026年8月19日2 次浏览来源:Dev.to阅读原文

正文保留英文原文(机翻易破坏代码与排版),标题/摘要已提供中文

No token.

One IP.

July 29, 2026: Three conditional requests.

Three .

Zero bytes of body across all three.

Three requests gone from a bucket of 60 per hour.

I opened the terminal to write the opposite post.

The short version: if you call the GitHub REST API without an header, an request that comes back still decrements .

The ETag saves you bytes.

It does not save you quota.

GitHub's documentation states the claim five times on one page and attaches the condition to two of them, and that clause falls off easily when a sentence gets quoted on its own.

The post I meant to write My working title was something like "poll GitHub for free with ETags".

I believed it.

I had read the sentence about responses not using your rate limit, I had repeated it to other people, and the plan was a tidy little piece with a before-and-after budget chart.

The first run killed it. went down.

My first reaction was that my counter reading was wrong, which is the normal reaction and usually the correct one.

It was not wrong.

So the post changed, and the finding turned out to be worth more than the one I went in with.

Does a 304 count against the GitHub rate limit?

What the docs actually say Here is the part that matters, and I want to be precise because it would be easy and dishonest to turn this into "GitHub's docs are wrong".

They are not.

On the page Best practices for using the REST API the claim shows up five times.

Two of the five carry a condition; three do not.

Here is the strict one, the only place on the page where the condition is spelled out as a header: "Making a conditional request does not count against your primary rate limit if a response is returned and the request was made while correctly authorized with an header." Nothing I measured contradicts that sentence.

There was no header on my requests, so the condition was not met, so no discount was owed.

I want to be careful about how much that proves, because it is less than it sounds.

A null result on the unauthenticated branch does not verify the authenticated one.

My numbers are equally consistent with two different worlds: one where the clause is load-bearing and the discount really does work once you send a token, and one where the discount no longer works for anybody and the documentation is simply stale.

I cannot tell those apart, because telling them apart needs a token and I do not have one here.

I have picked the generous reading throughout this post.

That is a choice, not a finding.

The second sentence that carries the condition is easy to walk past, because it never says at all.

It is a bullet near the top of the page, in the "Avoid polling" list: "Make authenticated conditional requests, so that unchanged data does not count against your primary rate limit." One word is doing all the work there: authenticated.

Drop it and you have the advice I was about to publish.

I walked past that bullet on my first read of the page, which is funny in the wrong direction: I went hunting for the places where the clause falls off, and the place where it holds is the one I missed.

The other three drop the condition outright.

Two of them sit in the same section as the strict one: "This makes conditional requests especially useful when you poll an endpoint, because each response is fast and does not use your rate limit." "If the data has not changed, you will receive a response, which does not count against your primary rate limit:" The last one sits further down the same page, under the heading "Make requests that can be cached": "A conditional request only saves you time and rate limit if the endpoint returns ." In context the page is not contradicting itself.

All four examples on it carry , so the whole page is written for the authenticated reader.

Read top to bottom by a careful person, it is fine.

I read the page twice, three weeks apart: July 29 and again on August 19, 2026, right before publishing.

It did not change between those two readings, and that part is checkable rather than a claim about my memory: the source file behind the page, in , has not been touched since commit , dated July 27,

2026.

The bullet was sitting there the whole time.

Only my count of it changed.

But sentences travel alone, and the short unconditional ones travel best.

I did not count how often, so I am not going to tell you it is everywhere.

I can show you two places I checked.

The first is on GitHub's own community forum, in the discussion Working with the GitHub API rate limit, opened by in March 2026: "As a 304 is a NOT MODIFIED return, it does not count against our rate limits." No clause.

In July I filed that as one developer telling another what everybody knows.

It is not sitting there as that any more.

Re-reading the page on August 19, 2026, before publishing, I found a badge on it dated that same day: ✅ Verified by GitHub, on an answer by a Maintainer, with the note that the content "has been reviewed and verified by GitHub subject-matter experts for accuracy and quality".

The verified answer repeats the sentence above, still without the clause.

The badge is on the answer as a whole rather than on that one line, and I have no idea how deep the review goes.

One thing in his defence, because it is the whole point of this post: the example script in that same write-up builds its client as , reading from the environment.

In his own context, with a token, he is very likely right.

The sentence just does not survive being lifted out of it.

But I went looking for examples of the short version travelling on its own, and the best one turned out to have GitHub's own stamp on it, applied while I was writing this.

The second is me.

In a post about fingerprint spoofing on June 2 I wrote: "Send conditional GETs on anything you re-fetch. / .

Free bandwidth, fewer requests, and it makes your traffic look like a cache-aware client instead of a firehose." Free bandwidth was right.

Fewer requests was the part I had not measured, and I printed it anyway.

And notice who ends up holding the wrong belief.

With a token you get a documented 5,000 requests per hour, and the docs say your 304s are free on top of that.

Without one you get 60, and they are not free.

The discount is missing exactly where the budget is tight.

The probe, and the three controls that had to be able to kill it The claim here is about the arithmetic of a counter.

So the controls have to bite at the counter, not at the transport.

A probe that proves "HTTP works" proves nothing about counting, and I have shipped that mistake before.

Standard library, no token, no signup, runnable locally.

It spends 6 requests.

Output, my machine, pasted as it came out: The block is printed by the script, not added afterwards.

A probe that lists what it refused to measure is harder to over-read later, including by me.

One note on the byte counts.

The probe goes through , which sets on its own, so the log above shows the uncompressed body.

Any normal client asks for gzip and gets the same JSON in about 1.4 kB on the wire: I checked that separately with against the same endpoint, and , which unpacks to the same 5,996 bytes.

The same check three weeks later gave , which is the sort of wobble you get when the JSON changes by a star count.

Two measurements, 1,423 and 1,428, and no third number in between that I am rounding to.

So the ETag saves you roughly 1.4 kB per poll, not 6 kB.

Two things the script gets wrong about itself, since you are going to read its output.

The line calls the window fixed and says the whole bucket refills; that is GitHub's documented model, not something a 29-second run can see.

And , the block that lists what I refused to measure, does not list it.

The prose above is the accurate version: the reset epoch did not move while I spent, and that is all I saw.

The idle control, and the header that beats it My bucket did not start at

60.

It started at 32, with , because earlier runs the same evening had already spent from this IP.

That is the whole reason the idle control exists.

分享