Skip to main content

How Prism reads GitHub (and what the token needs)

Written for whoever owns your GitHub org and whoever issues the credential. Short version: your users never generate GitHub traffic, the background sync is a single serial rate-capped job, and the token it needs is read-only.

For the procedure of turning the source on, see Take a source live. This page is what to hand your platform and security teams before you get there.

1. Users never touch GitHub​

When someone asks Prism a question, the agent answers from a local read-only copy of the GitHub fields it needs, held in Prism's own Postgres. It does not call GitHub at query time for counts, rankings or per-person breakdowns.

One optional exception, off by default: the live drill-down (mcps.githubLive.enabled), which reads a specific pull request on demand — never a population. It is bounded on purpose: enumeration stops after three pages, and one turn may open a limited number of distinct pull requests before further reads are refused. See Limits.

2. The sync is one background job​

A single scheduled job refreshes that copy — by default every 6 hours. It runs one request in flight at a time: no parallelism, no fan-out, no burst. The GitHub and Jira syncs are offset so they never run together.

3. It's incremental​

After a one-time initial load (INGEST_BACKFILL_DAYS, default 90), every subsequent run pulls only what changed since the last run, using GitHub's updated: search qualifier — normally a small delta of a few pages.

Issues reach back a year, once the engine is ingesting GitHub​

ingest.backfillDays sets how far back the pull request walk goes. In ingest.engine.mode: authoritative (and for the shadow copy in shadow) the issue walk has a floor of its own: it reaches back at least 365 days, whatever ingest.backfillDays says. Lowering ingest.backfillDays cannot shorten it.

On a source the engine has not ingested before. The floor is applied only where the engine finds no recorded issue horizon. On an install where the legacy GitHub lane has already run, that lane's horizon is already in the shared ingest_state row, the engine reads it, and flipping to authoritative therefore keeps the depth you already had rather than deepening to a year. ingest.github.rewalkFrom is the lever that recovers it — see §9 for what a re-sweep costs.

365 days is a floor on the issue walk, not a cap. Set ingest.backfillDays to 365 or more and the issue lane walks the whole window with the pull-request lane — at 1095 both reach back three years. The manifest figure decides how deep the issue lane goes only while it is the deeper of the two.

That is a deliberate answer to a question people ask constantly and could not get: how much of the backlog is still open, year to date. A window of 90 or 180 days does not reach January from August, so the answer came back correctly labelled as covering less than was asked for — honest, and no use. A year covers year-to-date on any date, including 31 December.

It is affordable because the walk skips what it does not need. The two searches share one calendar-day-at-a-time walk, and a day older than the pull-request window is walked by the issue search alone, with no pull-request search over it and no GraphQL enrichment, which is the expensive half of a busy day. What that costs: the issue search pages per 100 results, so a deep day is one search request per organisation per 100 issues updated that day — one for a quiet day, two at the density the sizing measurement saw (14,386 issues over 90 days is about 160 a day), at most ten before the walk subdivides the bucket. At the default cap of 30 requests per minute that is roughly 9–18 extra minutes for one organisation on the default 90-day pull-request window (275 extra days at one to two requests each; the measured density sits at the top of that range), proportionally less on a 180-day one, and nothing at all once ingest.backfillDays reaches 365 — there are no days the issue search walks alone then, and the whole walk is already the one the sizing table covers. It is proportional to the number of organisations, like the rest of the walk.

Two consequences worth knowing before you plan a first install:

  • Lowering ingest.backfillDays for a fast first pass no longer shortens the issue walk. 7 days of pull requests still comes with 365 days of issues, which on its own is roughly 12–24 minutes per organisation at the default cap (358 issue-only days at one to two requests each, by the same arithmetic as above). Raising it past 365 does still lengthen the issue walk, because the manifest figure is a floor and not a cap — at ingest.backfillDays: 1095 both lanes walk three years, and the issue lane costs a full bucket per day, exactly as the pull-request lane does.
  • Under ingest.engine.mode: off (the default before 0.10.0), none of this applies. The legacy lane walks ingest.backfillDays for issues and pull requests alike, exactly as it always has.

And one asymmetry to know about, because nothing labels it yet: the link between an issue and the pull request that closes it is collected from the pull request side, so it exists only for pull requests inside ingest.backfillDays. Counts, ages and states of older issues are complete; "is this issue linked to a pull request" is not, for issues whose only pull request is older than the pull-request window. Raise ingest.backfillDays if that question matters to you as much as the backlog one does.

4. It's scoped, lightweight, and read-only​

Prism reads, and stores, only these:

FieldSourceWhy
Repo, PR number, state, titlePR objectGrouping, keys, display
Author loginPR objectAttribute output to a developer
Created / merged / closed timestampsPR object"Merged in window", cycle time
Additions, deletions, changed filesGraphQL PullRequestPR size as an effort proxy — counts only, never the diff
Reviewer login, review outcome, timestampGraphQL PullRequest.reviewsReview load, time to first review — outcome and time only, never the review text
Ready-for-review timestampGraphQL ReadyForReviewEventWhen the review clock starts on a draft
Issue repo + number, state, state reason, timestamps/search/issues (type:issue)Bug-backlog questions; open vs. closed, completed vs. dropped
Issue author login, assigneessameWho reported it, who is carrying it
Issue title, labels, comment countsameDisplay, classification (priority is a label) — the count only, never the comment text
Issue typesameBug questions across both classification axes — your organisation's own type names (Task, Bug, …), stored verbatim as it wrote them
Which issues a PR closesGraphQL closingIssuesReferencesTie a fix back to the bug it closed
Commit author email + the login GitHub matched it toGraphQL PullRequest.commitsJoin a Jira assignee (known by email) to a GitHub author (known by login) — off unless you list your domains, see below

What Prism does not read, and does not need:

  • Source code, file contents or diffs. It takes the numbers, never the code.
  • Review or comment bodies. Outcome and timestamp only.
  • Any write. No creating or editing pull requests, comments, statuses or branches.
  • Admin, org settings, secrets, Actions, packages or webhooks.

What your GitHub must expose for people to be correlated​

Everything above is about pull requests, reviews and issues, and none of it depends on this. This part is about people, and it is a prerequisite rather than a tuning option: no setting on our side substitutes for it.

Every GitHub record Prism stores names a person by their GitHub login and by nothing else — author, reviewer, assignee, issue reporter. No GitHub dataset carries an email address. Jira, by contrast, knows a person by their corporate email address, and Agent Router spend is resolved to a person through your directory. So something has to say that a login and an address are the same person, and there are exactly three places that can come from. An installation needs at least one of them, and on a stock 0.11 install it has route C without doing anything.

RouteWhat it needs from youIf it is absent
A — your directory carries the loginThe source you designate as the organizational context stream holds a GitHub login per person, beside their emailPrism cannot invent it. Nothing else it reads knows who a login belongs to
B — the commit-author harvestDevelopers commit with a corporate address, GitHub matched that address to an account, the address is not a @users.noreply.github.com one, and you have listed the domain in ingest.github.identityEmailDomainsNo email-to-login evidence is ever collected, so there is nothing to build a crosswalk from later either
C — the profile sweep (0.11, on by default)Your members publish an address on their GitHub profile, and your credential carries Members: Read. Nothing else — it runs on the ingest tick, so in shadow and authoritative alike (install-wide ingest.engine.mode: off renders no tick and therefore no sweep), and needs no identityEmailDomains (empty admits every published address; a value you set narrows the sweep to those domains)The sweep records nothing, and route C contributes nothing. It is the route most installations will actually be resolving through, so this is worth checking rather than assuming

Route C is the one a stock install has, and route A is the one to plan for — it is the record once you designate a stream, and the only route that resolves somebody who has published no address anywhere. They add up rather than compete.

Route B is not the fallback it looks like. It is off until you name domains, it reads addresses out of commit metadata rather than out of your own organisation's records, and it does not run at all while GitHub is authoritative on the ingest engine — the default since 0.10.0. Anything below that describes "what a GitHub instance yields on its own" is now route C's job; route B is a supplement to it, on an install that has deliberately turned it on.

The two ADDRESS routes converge on a requirement that is easy to miss: B and C resolve a person only where the address they find is one your designated directory already holds. A perfect sweep against a directory spelling the domain differently resolves nobody. If your yield is low, compare the domains first — a local-part mismatch is rare and a domain mismatch is not.

Route A is the exception, and it is most of why it is worth planning for: a directory that carries the login states the fact outright, so nothing has to match on an address at all.

What you lose if none of the three holds. Not "some answers get less accurate" — these have no answer at all, and Prism says so rather than producing one:

  • anything that puts a person's GitHub work beside their Jira work — pull requests merged against issues closed, review load against story points;
  • anything that puts a person's GitHub work beside their Agent Router spend;
  • every team-shaped GitHub question — a team is a property of a person in your directory, so with no route from a login to a person there is no route from a login to a team either.

GitHub-only answers are unaffected: counts, rankings, cycle time, review latency and the issue backlog all work, grouped by GitHub login. What you do not get is a person behind that login.

The estate-wide blocker to check for first. GitHub's "Keep my email address private" setting rewrites every commit that user makes to a @users.noreply.github.com address. Such an address resolves to a login perfectly well, which is exactly why Prism drops it: it joins to nothing a person is known by anywhere else. An organisation where that setting is on by default harvests zero pairs, and no configuration on our side changes that. The same applies to a GitHub Enterprise Server instance running the feature, which issues the equivalent addresses under its own host name.

Find out before you commit to a route. prism-ingest check github measures route B against your own instance — how many sampled commit authors carry an address at all, how many of those GitHub resolved to an account, and how many fall inside the domains you configured — as counts, without recording a single address. Zero in the first of those and route B is dead on this instance whatever else you set. See Self test for where the verdict appears, and the sections below for what each number means.

It does not yet measure route C, which is the one on by default — so there is no way to predict the sweep's yield before turning it on, and the place to read what it actually produced is the self test's identity crosswalk line afterwards. Tracked; until it lands, treat a low github_login:observed count as the measurement.

Email addresses: nothing is recorded until you say which domains may be​

That is route B, and Prism derives it from the commits it already fetches: a git commit carries its author's email address, and GitHub tells us which account it matched that address to. It records none of it by default.

# values.yaml
ingest:
github:
identityEmailDomains: "" # empty (the default): no address is recorded

Set it to your own domain(s) — acme.com, acme-labs.com; a leading @ is fine — and Prism records an address only when all of these hold:

  • the domain is one you listed. Subdomains are not implied: list contractor.acme.com separately if you want it;
  • GitHub resolved the address to an account. An address it could not place is a string somebody typed into git config, and Prism will not guess who it belongs to;
  • and it is not one of GitHub's own @users.noreply.github.com addresses.

What lands is evidence, not a decision: a table of "this address was seen against this login, this many times". An address seen against two different logins stays two rows — the disagreement is preserved rather than resolved, because nothing here can tell which of the two is the person. Prism records the pairs and acts on none of them by itself: nothing is renamed, nothing is merged, and no record you keep elsewhere is changed.

Measure before you turn it on. How much this yields depends entirely on your instance: whether your developers commit with their corporate address, and whether GitHub has that address registered against their account. prism-ingest check github reports exactly that — how many sampled commit authors carry an address, how many are in your domains, and how many resolve to an account — as counts, without recording a single address. The Self test page shows the verdict.

Turning it on reaches back. The first run after you set the domains walks the whole backfill window again, so the people already in your mirror are seen — without it the harvest would only ever notice someone the next time they opened a pull request, and the table would look empty on an install that has been running for months. It costs one extra walk and re-fetches pull requests you already have, which changes nothing. Adding a domain later does the same thing again, for the same reason.

Prism tracks this per day, per organisation: a day is recorded as harvested only once it has actually been read for authors. The harvest rides the same GraphQL call as PR sizes and reviews, so a day that call failed on is a day whose authors were never read — it stays unharvested, and a later run picks it up. Nothing has to be reset by hand.

A day can also be harvested but not whole: GitHub answers with the pull request and withholds its commit list. What happens next depends on why.

  • Refused (the repository is one your token can search but not read): the day is counted in identity_partial_days and is not re-walked, because no re-run crosses that boundary. The fix is widening the token's repository access, then a one-off re-sweep over the affected dates — see Widen a source's scope. Unlike the count above, this one does not fall on its own.
  • Busy (a timeout, a rate limit, anything transient): the day stays unharvested and the next scheduled run reads it. Nothing to do.

A reason Prism does not recognise is treated as the second kind, deliberately: retrying costs one extra walk, while assuming the worst would write off evidence that was there to be read. A day that keeps failing that way is retried a few times and then becomes a floor of its own, so a re-walk can never go on for ever.

Where these numbers live. They are in each run's log, and durably in the ingest_state row for the github source — identity_uncovered_days (still to be harvested), identity_partial_days (harvested, not whole) and identity_floor_reasons (why). There is no admin screen for them yet, so reading them outside the log means querying the database. If the backfill window is being re-walked every run, identity_uncovered_days is the count still outstanding: falling to zero over a few runs is the sweep completing normally.

It does not run in authoritative engine mode. If GitHub is running authoritative — by the install-wide ingest.engine.mode, or by ingest.github.engineMode, the per-source value that decides this source on its own — it is ingested by the manifest engine, and GitHub's manifest declares no commit-author step. Pull requests, issues and reviews are unaffected, and this harvest collects no commit-author evidence: ingest.github.identityEmailDomains has no effect on it for as long as that mode is on. It is not inert for GitHub — the profile sweep (route C above) runs on the ingest tick whatever mode GitHub is in, and the same setting narrows it. (The engine itself is no longer without the capability — GitLab's manifest declares one, and the same setting gates it there. This is a statement about GitHub's manifest, not the engine's.) The ingest says so in its log every run and prism-ingest check github reports it as identity.harvest_runs_in_this_mode: false.

That is an accepted consequence of the flip, not a defect waiting on a fix. Prism keeps no directory of its own: the people in your organisation come from the source you designate as your organizational context stream, which is the designated route to a directory. The crosswalk still folds in any evidence this harvest has left behind, so the effect is exactly this — an install running GitHub authoritative resolves people to GitHub logins through the profile sweep and the organizational stream, without the commit-author route. So it is a choice between two supported shapes rather than a loss: keep GitHub on off or shadow if you want the commit-author evidence to go on accruing, and see Flip a source to the ingest engine for the flip, the rollback, and what else changes with it.

Every run then says what it took and what it refused, in its own log:

identity: this run saw 42 email/login pair(s) in 318 commit author sighting(s)
and dropped 96 (domain_not_allowed 71, no_account 19, noreply 6); the mirror now
holds 210 pair(s). Evidence only — identity_observations; nothing resolves a
pair to a person.

"Sightings" counts the addresses it kept, and the drops are counted beside it — so the two do not add up to every commit author it looked at, and are not meant to.

5. What the token needs​

A fine-grained token, or a GitHub App, is strongly preferred — see the warning under "classic" below. The permission list is the same for both; what differs is how the credential is configured and whose visibility decides its scope. A fine-grained token scoped to All repositories covers an organisation of any size; an App is preferable at any size because the grant is the scope rather than a person's visibility, and because it carries no user seat and higher rate limits — see Widen a source's scope, which is where the App's three values go and why it is not a token you paste.

Changing this credential's reach does not change the history already mirrored. Re-scoping a token from a hand-picked list to All repositories, or swapping a token for an App installed across the organisation, takes effect from the next sweep onward. Every day already walked keeps the scope it was walked under, no ordinary run revisits it, and nothing turns red — the two verdicts that mark an incomplete day, capped_days and unenriched_days, both describe rows the walk knew it missed, and these are rows it never knew existed. Recovering the history is a one-off ingest.github.rewalkFrom, and the procedure is Widen a source's scope.

Fine-grained PAT, or a GitHub App installed on the org​

  • Repository access: the in-scope repositories, or org-wide.

  • Repository permissions:

    • Metadata: Read — mandatory baseline.
    • Pull requests: Read — the core permission. Covers the PR list, review outcomes, and additions/deletions/changed-files.
    • Issues: Read — required since Prism began answering bug-backlog questions. Without it the pull-request half still works and the issue half ingests nothing.
    • Contents: not required. Grant Read only if your instance will not return PR size without it; Prism never reads file contents either way. (Required if you enable the live drill-down — see below.)
  • Organization permissions: Members: Read — required since 0.11.0, and the only org-scoped grant Prism asks for.

    ONE call needs it: the identity sweep's GET /orgs/<org>/members, which lists the logins whose public profile address is then read one at a time (ingest.github.profileEmailSweep, on by default).

    The other org-scoped call, GET /orgs/<org>, is a reachability pre-flight and a fine-grained token can still make it with no organization permission at all — that was measured, and this page said so before the sweep existed. It is spelt out again here because an earlier draft of this section quietly folded the pre-flight in with the sweep, which would have had a reader believe the grant was needed for more than it is.

    Without it the ingest still works and the answers get worse in a way that is hard to see. Pull requests, reviews and issues all mirror normally; what fails is the join from a GitHub login to a person, so any question scoped to a team, a manager's organisation or a named individual resolves to nobody. The sweep reports the refusal in the ingest tick's log and nowhere else.

    Set ingest.github.profileEmailSweep: false if you would rather not grant it. That is a supported configuration — it trades per-person attribution for one less permission — and turning it off is louder than being refused, because the tick then says on every run that no login will be resolved.

If the optional live drill-down MCP is on (mcps.githubLive.enabled), this same token serves it — the chart's ingest Jobs and the live MCP both read secret key github-token, compose gives both GITHUB_TOKEN, and the Cloud Run deployment mounts one secret into all three consumers. It opens one named pull request on request and can return that PR's diff, changed files, comments and CI checks, so add three grants to the list above, all Read: Contents, Checks, Commit statuses. The last two are what the CI-checks view needs — they are separate GitHub permissions, and without them that view fails with Resource not accessible by personal access token while everything else works. Trimming the list the other way — to what the live MCP alone needs — is what drops Issues: Read and empties the issue half. One token, one union, until it can hold its own (prism #572).

Classic PAT​

read:org plus repo read — read:org covers both the org-scoped search and, since 0.11.0, the member listing the identity sweep needs. On a classic token that one scope carries both, so there is nothing extra to add. On private repositories, "repo read" is the broad repo scope, which also grants WRITE. There is no read-only classic scope for private repos. If your security review requires a genuinely read-only credential — and for most organisations it does — use a fine-grained token or a GitHub App instead.

Two things that are easy to miss​

  • If the org enforces SAML SSO, the token must be SSO-authorized for that org, or it will authenticate and then see nothing.
  • A fine-grained token returns no x-oauth-scopes header at all, so an empty header is not evidence of a problem. Take a source live has commands that test what the credential can actually reach, which is the check that matters.

6. Where it connects​

DeploymentRESTGraphQL
github.com / Enterprise Cloudhttps://api.github.comhttps://api.github.com/graphql
Enterprise Server (self-hosted)https://<your-host>/api/v3https://<your-host>/api/graphql

Prism polls on a schedule; no inbound webhook is required. It needs egress to that endpoint from where it runs — see Egress.

GraphQL is a separate hop, and an Enterprise Server can serve REST while blocking it. If that happens you get pull requests with no size or review data rather than an outright failure, so it is worth testing both. The run names the days it could not enrich, in unenriched_days — see Go-live.

7. There's a hard rate cap​

ingest.github.maxRequestsPerMinute sets a fixed ceiling on calls to GitHub. Because the sync is already serial, this is a true requests-per-minute limit — no burst can exceed it, and retries and back-offs are counted against it too, so a job being throttled upstream cannot speed up.

# values.yaml
ingest:
github:
maxRequestsPerMinute: 30 # the shipped default; 0 disables the cap

The default of 30/min sits at GitHub's tightest documented budget for the search endpoint the walk uses. Set it lower if your platform team asks you to — it costs the first backfill time, not data. Limits has the detail.

8. What happens when a backfill is interrupted​

Relevant if you have set a low rate cap, because a slow backfill is a long one.

Progress is kept. The job records how far it got after each completed day of history, and a later run picks up from there — it does not start again and it does not re-truncate what it already fetched. A pod killed mid-run, an evicted node, or a run that hits its deadline all resume from the last completed day.

Two consequences worth planning for:

  • The first live run empties the table before it refills it, so that a deployment moving off seeded demo data does not serve a mix of the two. Until the backfill completes, Prism answers from a partial mirror and says so — coverage, staleness and "these figures are a floor" are reported with the answer.
  • Do not change ingest.github.orgs while the first backfill is running. Progress is only trusted for the orgs that produced it, so adding one correctly discards it — and starts the backfill over, truncation included.

If you want an interrupted backfill to resume sooner, shorten the schedule. Do not shorten ingest.runDeadlineSeconds. A resumed run re-walks the last day it completed before moving on — deliberately, because a day finished at 14:00 does not yet hold what was updated later that day — so each run needs long enough for two busy days of history, not one. Below that the job restarts for ever and the backfill never finishes.

The shipped default (24 hours) has hours of headroom at any sane rate cap, so this is a warning about lowering it rather than a reason to raise it. If a backfill is not completing, the tell is that the day it resumes from stops changing between runs — the row count and the logs otherwise look like ordinary interruptions.

9. Issue types, and the one upgrade that needs a re-sweep​

From this release the issue mirror also holds GitHub's issue type — the org-level classification (Task, Bug, Feature, or whatever your org configured) that sits beside labels rather than among them.

It matters because a question about bugs means both axes at once. An organisation that turned issue types on typically stopped applying the bug label, and one that never turned them on still uses it, so where an organisation uses both, counting either axis alone undercounts the backlog — on the repository this was found in, the labelled issues were a minority of the union. Where an organisation never enabled types, the label axis alone is the whole backlog and nothing is missing; gh api orgs/<org>/issue-types tells you which case you are in, and is worth running before you act on any of this. Prism answers a bug question as the union of the two and reports which axis found each issue, so an answer reads "N open bugs: X typed, Y labelled, Z both" rather than a bare number.

Answering across both axes needs the semantic query MCP, which ships on since 0.10.0. The union, and the "which axis found it" breakdown, are served only by that component, so if your values file pins mcps.semanticMcp.enabled: false (see docs/install/parameters.md) bug questions keep being answered on the label alone, however full the type column is. The re-sweep below fills the column durably either way — it is stored, not computed at question time — but nothing you gain from it is visible until the flag is on.

This one field is not recoverable from what is already stored. Every issue already in the mirror carries an empty type until the lane fetches it again, and an empty type is indistinguishable from a repository that has none — so until the re-sweep below has been all the way round, the typed half of a bug figure is missing or short. Nothing in the answer says so: a bug figure taken part way through the sweep reads exactly like a complete one, so treat the typed half as partial until you know the sweep has finished.

To fill it in, re-walk the issue history once with the standard re-sweep lever ("Widening the window later" in docs/install/go-live.md has the full procedure):

# values.yaml
ingest:
github:
rewalkFrom: "2026-03-01" # a date at or before the issue horizon

Two things to get right:

  • Know what your date buys. A date at the mirror's issue horizon re-fetches everything already mirrored and fills in its types — the complete type re-sweep. A date earlier than the horizon does more, not nothing: it widens the mirror itself, pulling in issues last touched back then that were never mirrored at all, at the same per-day request cost — that is the "Widening the window later" procedure in docs/install/go-live.md, and it is a coverage decision, not part of the type refresh. The issue horizon is recorded separately from the pull-request horizon that Admin → Self test and Admin → Context streams report, and no admin surface shows it today; read it from the mirror directly:

    kubectl exec -n <namespace> statefulset/<release>-postgres -- \
    psql -U postgres -d prism -c \
    "SELECT detail->>'issue_backfill_horizon' FROM ingest_state WHERE source = 'github';"
  • A re-sweep costs what the original backfill cost, so give it the same room: check ingest.github.maxRequestsPerMinute and ingest.runDeadlineSeconds before you start, and expect it to take hours at a low cap. Nothing is truncated and no coverage already earned is given up.

The value is carried out once per date — the run that performs the re-sweep records the date, so leaving it in your values file does not re-walk the same months at every firing of the schedule.

If you would rather not spend the requests, nothing breaks — but the figures do not stand still either. The label axis is the floor, not a resting state: every ordinary scheduled run writes the type on each issue it touches, so the typed half fills in gradually as issues see activity, so an un-re-swept bug figure may drift upward over the following weeks for a bookkeeping reason as well as a real one — and the delta on its own does not say which. If the organisation does not use types there is no drift from this cause at all. The answer will not tell you any of this — it carries no warning about an empty or part-filled type column, so the figure reads as complete whatever state the column is in. That makes two things yours to hold, not Prism's: knowing whether this re-sweep has run, and never taking a typed count of zero as a finding about the repositories — an organisation that never enabled issue types, one that has them on but leaves bugs to the label, one whose bug type is named something else (type names are org-defined free text, and this axis matches the literal bug, so a Defect or Incident type reads as zero with a fully populated column), and a column the sweep has not reached yet all produce the same zero, and the column cannot tell you which you are looking at. gh api orgs/<org>/issue-types is what tells you.

What that means in practice​

  • Steady state: each run fetches only what changed — a handful of requests.
  • Initial backfill: one org over 90 days is on the order of a thousand requests; at the default cap, tens of minutes. At a low cap, plan in hours, and see §8.
  • Peak concurrency against GitHub: one request. Not one per user, not one per page in parallel — one, full stop.

You hold the levers: the org scope (ingest.github.orgs), the backfill window (ingest.backfillDays), the schedule, and the hard cap (maxRequestsPerMinute). If you want it gentler, turn any of them down.