Skip to content

Cost of a sweep

Measured on 2026-09-11 with the real binary against the live API, every family switched on and every request logged with the x-ratelimit-resource and x-ratelimit-used headers of its own response. Cold is the first sweep of a fresh install or of a restarted service: an empty ETag cache, a month of workflow runs. Steady is the third sweep of the same process a few minutes later, when about four fifths of its requests were answered 304 and cost nothing; the second sweep is the one that pays the fill-in the actions row describes. A 304 is free, and it is only ever available to REST: GraphQL carries no ETag, so its column is the same in both sweeps.

The figures are per repository where the family asks per repository, and per sweep where it asks about the account. core is the REST bucket, pt a GraphQL point. Families with a request that is not core say which bucket it went to: search (30 a minute), webhook_deliveries (500 a minute) and dependency_sbom (100 a minute) each have their own. A row the round of 2026-09-11 changed describes the requests the code makes now and says what the two sweeps measured where that differs. There is no total row on purpose: a total belongs to one account and ages with it, so multiply the per-repository rows by the number of repositories -list prints and you have your own.

FamilyCadenceColdSteadyNotes
account12h1 pt1 ptone query: the calendar, the totals, the pins, the sponsors block and the star lists; 66 KB, never compressed, never conditional
achievements24h1 page off budget, 1 pt, plus the co-authored walkthe samethe badges come from the public profile page, read without the token and charged to no bucket. The progress rows beside them do come from the API: one counts query, and a walk over the merged pull requests whose co-authored commits no count exposes. Search stops at a thousand results, so the walk asks the whole life of the account and splits any range that overflows into two, one point per page and one per split, which is tens of points on a long-lived account and one on a new one. All of it once a day
totals12h1 core, 1 search, 3 pt0 core, 1 search, 3 ptthe profile is a 304 from the second sweep on; ten of the eleven counts are one GraphQL query of ten aliases, 406 bytes at cost 1, measured identical to the REST answers of the same minute; only the commit count is still a search, charged every time; a refused counts query is asked again one count at a time, ten points on that sweep only, so one failed search loses one number as it did in REST
ratelimit15m1 pt1 ptGET /rate_limit is free; the point is the GraphQL half of the same question
events30m3 core1 corebefore the round: three pages of a feed that changes every time, so If-None-Match never matches. Now: the walk stops at the page that carries the newest event of the previous sweep, remembered as last_event in the state file, which is usually the first page: 1 core a sweep, 96 a day fewer; a first sweep and a backfill still read the three
notifs30m20 core1 corebefore the round: twenty pages of fifty; one new notification shifts every page, so the ETag never matches, for six or seven rows that were new. Now: since= the newest updated_at seen minus two cadences (last_notified in the state file), which is one page for a two hour window: 1 core a sweep, and the twenty once a day (last_full), on a backfill, and on the first sweep after an upgrade, read threads included (all=true): that bounds to a day whatever GitHub’s filter left out of the windowed reads, and it is the read that closes a thread, since a sweep lists unread threads only and a thread read without a reply leaves that listing rather than coming back with its unread tag changed. 912 core a day fewer
billing6h2 core1 coreone per month walked; the month in progress changes, the previous one is a 304
profile12h4 core plus 1 per package0 corethe profile, the social accounts, the gists, the packages and one page of versions per package; all 304 after the first sweep
outbound12h8 pt8 ptall GraphQL: the starred list is one query of a hundred (13.5 KB against 617 KB decompressed from REST), the five searches one point each (7 KB against 100 KB), the two comment walks one each; nothing here has an ETag on either path, so the two columns are the same
historyoff1 pt per past yearthe samethe account’s whole calendar, once, at a point per year it has existed
keys24h2 core0 corethe SSH and the GPG keys
traffic6h4 core per repo0 coreviews, clones, referrers, paths; the fourteen-day window is a 304 until it moves
repo1h3 core per repo, 2 pt per 10 repos0 core for a repo that did not change, 2 pt per 10 reposthe repository, its community profile and one page of releases each; languages, topics, rulesets and branch protection ride in one GraphQL query per ten repositories
branches24h1 pt per 14 repos1 pt per 14 reposone query per fourteen repositories
stars6h1 core per repo1 pt per 10 reposthe full walk through REST the first time a repository is seen, then the newest hundred of every repository in one GraphQL query per ten, about a kilobyte per repository; a restart with a state file starts at that query. Before the round the last page was asked of every repository on every sweep, all but one of them 304
issues1hup to 8 pt per repo1 to 2 pt per repobefore the round: a page of fifty with ten review threads each, 8 points, which the gateway refused with a 502 once a sweep on the busiest repository. Now: 2 points of GraphQL per repo for what changed in two cadences, ten at a time (1 point where the repository holds five or fewer), and once a UTC day a whole page sized to the repository from the last totals (5, 10, 20 or 50 cost 1, 2, 3 or 8). The sweep that carries the daily page is the expensive one, and the busiest repositories still draw the gateway’s 502 or 504 at fifty once a day and are retried at twenty-five
issueevents1hseveral pt per repo for the month, plus 1 core per stacked pull request in it1 pt per repo, 0 to 1 corebefore the round: one page of a hundred events each, up to a megabyte per repository because every event embeds its whole issue; a 304 on all but the repository that moved. Now: one GraphQL query per repository, the timeline of the ten most recently updated issues and ten pull requests for the events of the last two cadences (1 pt, 2 to 6 KB, a second page only when more than ten items moved), plus one core per pull request in a stack, whose added_to_stack event the timeline cannot name; thirty days on a first sweep, once, which is minutes rather than seconds where the pull requests are nearly all stacked, because almost every one updated in the month takes the per-issue road for its added_to_stack; a steady sweep is a point per repository and one such read at most. And from a cadence before the last run after a gap, so a stopped process does not leave its hours out of the series. Measured against the list over a week of two repositories: every event of every type agrees, field for field, except a commit referencing an issue nobody has touched, 3 of 2,217, which does not move the issue and so is not asked for. A backfill walks /issues/{n}/events per item instead of the list, 1.4 KB compressed per twelve events against 45 KB per event
actions15ma month of runs per repo, plus 1 core per run1 core per repo that had a run, plus 1 per run completed sincebefore the round: pages of a hundred runs reaching a month back, a job list per run, and the caches and workflows of every repository, which was three fifths of the cold sweep’s bytes, and those job lists again on every sweep, nearly all of them 304. Now: pages of 30, one on a quiet repository and up to 7 while they come full of runs newer than the window, plus one per run not yet expanded, jobs listed once per attempt, at most 20 new runs a sweep; pages of 100 on the first sweep and in a backfill. Measured over three sweeps of one process, the second is the one that pays the fill-in, because the page of thirty is a new URL for every repository and holds runs the first sweep’s twenty did not cover; from there a sweep costs one page per repository that had a run and one job list per run completed since, so what it costs is how many runs the account completes
artifacts1hup to 5 core per repo0 to 5 core per repoa busy repository fills its pages, and all five of them are charged again whenever an artifact was added or expired: 5 on one sweep, 0 on the next
security1h2 core per repo0 coreDependabot and code scanning alerts; most are the 403 of a repository with Dependabot switched off or the 404 of one without code scanning, and a refusal carries no ETag, so each was charged again on every sweep before the round remembered them. Now: a refusal is answered from memory for a day, per family, repository and endpoint, so the steady figure is 0 core on the second sweep and one request per refusal once a day. The rest are conditional requests, all 304
stats12h3 core per repo0 coreparticipation and the punch card; GitHub recomputes them slowly and answers 304
discussions2h2 pt per repo with a forumthe samebefore the round: fifty threads with twenty comments and twenty replies each, 11 points, asked of every repository, including every one with no forum. Now: 2 points per repo with a forum, the ten most recently updated threads with the same twenty comments and twenty replies each (comments come oldest first, so a shorter page there would stop recording the eleventh comment of a thread); a repository whose forum is off is never asked
commits1h1 pt per repo1 pt per repothe last two cadences of the default branch; a month back on the first sweep, which is a few hundred kilobytes for a busy repository
activity30m2 core per repo0 to 2 core per repothe repository log, a hundred entries per page; charged only for a repository whose log moved, 2 on one sweep and 0 on the next
analyses6h1 core per repo0 coremost are the 403 and 404 of repositories without code scanning, charged again each sweep before the round; now remembered for a day, so the steady sweep is conditional requests and 0 core
forks12h1 core per repo1 pt per 10 reposone page each through REST on the first sweep of a fresh install and in a backfill, then the newest hundred of every repository in one GraphQL query per ten, under a kilobyte per repository; a repository the batch reports holding more than a hundred forks is walked through REST as well, since a fork row’s stars and days_since_push move and the batch cannot refresh the rows past its page. Before the round: one request per repository a sweep, all 304
planning6h1 pt per repo1 pt per repolabels and milestones
joblogsoff1 core per repo, 1 blob per failed job0 corethe failed runs of the last hour per repository, asked of a list filtered to the month a re-run can reach back to, and one blob per failed job from object storage, outside the API’s quota; the filter’s URL changes once a day, so a day costs one charged page per repository and the rest are 304
settings6h2 core per repo, 1 webhook_deliveries per hook0 core, 1 webhook_deliveries per hook that movedwebhooks, environments and deploy keys; the deliveries of every hook are charged to their own bucket
rulesets24h1 core per repo plus 1 per ruleset0 coreone list per repository and one history per ruleset, both with an ETag; none of it charged on a day nobody edited a ruleset, when the steady sweep asks the same questions and every one of them is conditional
inventory24h4 core per repo0 corethe workflow token policy, the secrets, the code scanning setup; the refusals were the 403 of repositories without it, now remembered for a day, which at this cadence is the next sweep anyway; the rest are conditional requests, all 304
deployments1h1 pt per 5 repos1 pt per 5 reposone query per five repositories, the newest hundred deployments each
policyfiles24h1 pt per 5 repos1 pt per 5 reposone query per five repositories, the history of four paths each
depsoff1 core and 1 dependency_sbom per repo0 core, 1 dependency_sbom per repo that received a commitone commit read and one SBOM per repository; the SBOM has its own bucket and most were 404, now remembered for a day. GitHub regenerates a SBOM on every read and its ETag never matches, so the photograph is taken only when the head moved since the last sweep (the head itself is a free 304 when it did not): 0 dependency_sbom on a repository without a commit, one per repository that received one, and one of the SBOM reads timed out on GitHub’s side on each of the three sweeps measured

What weighed before the round was not REST but GraphQL: issues and discussions were 88 % of the points, because both queries were sized for a backfill and asked on every sweep. The only search is the commit count of totals, one a sweep against a budget of thirty a minute.

The round of 2026-09-11 lowered several of these rows, and the first measurement is the baseline it is measured against: the job lists a run has already had expanded are not asked for again, the runs page is thirty on an ordinary sweep, discussions is asked only of repositories with a forum and for ten threads, pulls is sized to the repository and windowed to two cadences, notifications and the event feed stop at what the previous sweep saw, a 403 or 404 is remembered for a day instead of being charged again every hour, and the newest stars and forks, the starred list, the outbound searches and ten of the eleven totals counts moved from REST to GraphQL, the same rows for a point apiece instead of a request apiece. Measured again after the round, three sweeps of one process on the evening of the same day, the steady sweep charges about a third of the core and a third of the GraphQL points it charged before, moves half the bytes on the wire, and answers about four fifths of its requests with a 304. Projected to a day at the built-in cadences that is roughly an eighth of the REST calls and a third of the points, plus one job list per workflow run completed, which is the one term that grows with how busy the repositories are rather than with how many there are. Two things did not fall. The cold sweep charges more core than before, because the first sweep of issue events now reads the whole month through the timeline and, where pull requests are stacked, one per-issue list for nearly every pull request updated in it: minutes, once per process. And a sweep that runs every family at once still takes minutes rather than seconds, because its conditional requests cost a third of a second each and its GraphQL queries nine tenths, one after the other; the sweeps production runs are the 15 minute one and the hourly one, each a fraction of the whole.

GraphQL is the cheap one, by a wide margin

Section titled “GraphQL is the cheap one, by a wide margin”

One query returns the full 366-day contribution calendar, every contribution total, the per-repository commit breakdown and the social counts, for one point of a five thousand point budget. The same data over REST would be dozens of calls and would not include the calendar at all, because the calendar exists nowhere else.

That is why the account family runs on a twelve-hour cadence and still costs almost nothing, and why the expensive families are the REST ones that scale with how busy the repositories are.

Two families scale with activity, not with size

Section titled “Two families scale with activity, not with size”

Everything else costs a roughly fixed number of calls per repository. Two do not:

  • actions costs a page of thirty runs, up to seven while they come full of runs newer than the window, plus one request per run whose jobs this process has not written yet, at most twenty a sweep. A repository with continuous integration on every push generates runs continuously; a quiet one costs the one page, answered 304, and nothing else.
  • artifacts walks up to five pages per repository, and a busy repository fills them.

Set its interval to 0.

every:
families:
artifacts: 0
joblogs: 0

A family that is off writes nothing and costs nothing. Its dashboard panels go empty, which is the honest reading. See cadences.

Reading the arithmetic for your own account

Section titled “Reading the arithmetic for your own account”

A first sweep is the expensive one: the full stargazer walk, a month of workflow runs, and (with every.history set) every past year’s contribution calendar. After that, multiply the per-repository rows above by the number of repositories -list prints, and divide the hourly budget by the cadence.

A card is priced as a cold sweep whatever the cadences say: the run collects every family, because every number it draws comes from that one sweep, and its process starts with an empty ETag cache. So N cards are N sweeps, and a workflow drawing three of them pays three.

The signal that the sum came out wrong is a warning, not a guess:

level=WARN msg="rate limit reserve reached, family skipped" family=actions

Once in a while is fine. Every sweep means the cadences are too fast for the number of repositories.