The Repo Data Gap Reference: Complete, Capped, Windowed, or Broken
Look up any repo-derived field's source, name its window, cap, truncation signal, and dated breaks, and label it complete, capped, windowed, or broken with a cross-check.
Key takeaways
- GitHub's Events API returns at most 300 events and only from the past 30 days, so an events-based "no activity" flag is wrong for a maintainer who last shipped 45 days ago.
- GitHub search stops at 1,000 results per query, so when total_count is larger than the number of items returned, split the query into created-date slices until each slice is under 1,000.
- Only the first 500 author emails on a repository's contributors list are linked to GitHub users, and everyone after that appears as anonymous, visible only with anon=1.
- At 10,000 or more commits, stats/contributors returns a normal 200 with zero additions and deletions while code_frequency returns 422, so a large repo's zero line count is a cap, not a fact.
- Since 2025-10-07, GitHub PushEvent payloads carry only before, head, push_id, ref, and repository_id, so a push commit count of 0 is broken data whenever before differs from head.
- GH Archive's 2025-10-10 12:00 hourly file held 881 events against 166k on 2025-10-08, and no source shows those losses were ever backfilled.
I keep this lookup open before any repo-derived count, contributor table, or "no activity" flag goes from the data team to recruiting, sales, or an investment memo. It's for the recruiting ops or revenue ops owner, or anyone else who has to answer for how a number was gathered. Each GitHub source gets one row: its lookback window, its result cap, how the loss shows up in the response, and any dated break. The row ends with a label (complete, capped, windowed, or broken) and the cross-check that confirms it.
What do complete, capped, windowed, and broken mean?
Every repo-derived value gets exactly one of four labels. The label tells the reader whether a zero or a short list describes the repository or only describes the source. Treat a value with no label as unverified, not as complete.
Four terms come first:
- Lookback window: how far back a source keeps records before older ones stop being returned.
- Result cap: the most items a source will return for one query, however many exist.
- Silent truncation: data dropped while the source still returns a normal success response, with no error and no flag.
- Dated break: the day a source permanently changed what it records. Values from before and after that day can't be compared.
| Label | What it means | Typical trigger | Ship it? |
|---|---|---|---|
| Complete | Source returned everything for the span | Under cap, inside window, no break crossed | Yes, with source and pull date |
| Capped | Source stopped at a result limit | 1,000 search hits; 500 emails; 10,000 commits | Not until cross-checked |
| Windowed | Span is older than the source keeps | Events past 30 days or past 300 events | Not until cross-checked |
| Broken | Span crosses a permanent loss | Payload trim 2025-10-07; GH Archive drops | Only flagged, after a rebuild |
When more than one label applies, record the most severe one. Broken outranks capped, and capped outranks windowed. Re-querying the same source can't fix a broken value. A capped or windowed value can usually be fixed by re-querying a different way.
Why the label has to travel with the value
The people who read a repo-derived export are rarely the people who could audit it. So the label belongs in an export column next to the number, not on a wiki page nobody opens mid-search.
| Measure | US | UK |
|---|---|---|
| Technical recruiter/sourcer profiles | 21,716 | 729 |
| Of those, listing GitHub as a skill | 39 | 3 |
| Share (derived) | 0.18% | 0.41% |
The US base is 29.8 times the UK base, but the US GitHub-skilled count is only 13 times larger. Still, a UK count of 3 is too small to rank or compare. A skill tag is also a blunt proxy for "can read API output". Even so, the result points the same way in both markets: the person who receives a contributor table will usually take its zeros at face value. In the US index, the GitHub-skilled subset clusters at a handful of employers (Meta with 2, then SpaceX, AWS, and Trexquant). Expertise is concentrated, not spread through the teams that consume this data.
Source gap lookup: window, cap, signal, and break per source
Six GitHub-derived sources cover most pipelines. Each one drops data in a different place, and each signals the loss differently or not at all. Find the row for your field's source, then read its subsection for what the value proves and how it misleads.
| Source | Window / cap | Signal when hit | Dated break |
|---|---|---|---|
| Events API | 30 days / 300 events | None (silent) | 2025-01-30; 2025-10-07 |
| Search | 1,000 results | 422 past 1,000; incomplete_results on timeout | None found |
| Contributors | 500 emails linked | Anon rows (with anon=1) | None found |
| stats/contributors | 10,000 commits | 200 with zero additions and deletions | 2023-11-29 announcement |
| stats/code_frequency | 10,000 commits | 422 | 2023-11-29 announcement |
| GH Archive | Mirrors Events API | None (smaller files) | 2025-05-24; 2025-10-08 to 16; 2026-10-27 |
Where each layer drops data
- GH ArchiveMirrors the Events API, plus permanent volume and payload drops
- Events API300 events or 30 days, trimmed payloads since 2025-10-07
- Search and list endpoints1,000 search results; 500 linked contributor emails
- Repository statisticsZeroed or refused at 10,000 commits; merges excluded
- Git historyClone and log; the complete route the others are checked against
Events API
- Proves: an event happened within the past 30 days and is among the newest 300.
- Lies when: you read an empty or short timeline as "nothing happened". GitHub's docs say the timeline holds up to 300 events, only from the past 30 days, and older events are left out even when fewer than 300 come back.
- History: the window used to be 90 days. The old docs described 10 pages of 30 events. The cut to 30 days took effect on 2025-01-30, after a 24-hour test on 2024-12-03. It applies to the /events, /networks, /orgs, /repos, and /users event endpoints, plus the Atom feed, /timeline, and /dashboard-feed.
- Latency: events can take from 30 seconds to 6 hours to appear, so the newest activity can be missing too.
- Cross-check: the commits endpoint with a
sincedate, or git log on a clone.
Search API
- Proves: the first 1,000 matches exist.
- Lies when: you treat the items you got back as the full set. Search returns at most 1,000 results per query. When a query times out, it returns whatever it found and sets
incomplete_resultsto true. - Signal: asking for a page past result 1,000 (page times per_page over 1,000) returns a 422. One practitioner source gives the message as "Only the first 1000 search results are available".
- Real case: in one practitioner write-up, nf-core/test-datasets had more than 1,100 closed PRs that search could not reach.
- Cross-check: split the query into created-date slices until each one is under 1,000. For PR history, the same write-up recommends incremental GraphQL sync instead of search.
Contributors list
- Proves: the linked contributors among the first 500 author emails.
- Lies when: you count rows as people. Only the first 500 author emails are linked to GitHub users. The rest show up as anonymous, and you only see them when you request
anon=1. Someone who committed under several emails can be counted twice. - Freshness: the list is cached and can be a few hours old.
- Cross-check: request anon rows, then dedupe authors from git log.
Repository statistics
- Proves: commit counts for repos of any size, and line counts for repos under 10,000 commits.
- Lies when: a large repo's zeros get stored as truth. At 10,000 or more commits,
stats/contributorsreturns 0 additions and 0 deletions while still giving commit counts, andcode_frequencyreturns 422. GitHub announced this on 2023-11-29. - Other traps: stats can return 202 while GitHub is still calculating them. All repository stats exclude merge commits, and contributor stats also exclude empty commits. Even a complete stats value won't match a raw commit count. That's a difference in definition, not truncation.
- Cross-check: for large repos, GitHub's own changelog points to
git log --shortstat --no-merges.
GH Archive
- Proves: that the public event stream contained an event in that hour, as the archive captured it.
- Lies when: a series crosses one of its breaks. The archive mirrors the Events API, so it inherited the payload trim. It also has volume drops of its own that no error ever reports. The only sign is smaller hourly files.
- Cross-check: compare each hourly file's event count with nearby hours, refetch detail from REST by ID, or rebuild from git clones.
Two things are not in this table because I could not verify them: the window for traffic views and clones, and any top-100 cap on the contributor graph. If your pipeline uses either, compare a sample against git log locally before trusting it.
Why doesn't a 200 response mean the data is complete?
Most of the large losses in GitHub data come back as normal successful responses. Events windowing, GH Archive volume drops, and zeroed contributor stats all return 200. Only paging past search result 1,000 and code_frequency on a large repo return a 422.
The reason is simple: GitHub returns what it has rather than raising an error. A search timeout works the same way. You get a partial result and a flag, not a failure. Pipelines that only alert on non-2xx status codes will pass every one of these silent cases straight through.
The biggest gaps in GitHub data don't arrive as errors. They come back as a 200 that looks normal.
Three response states need explicit handling:
incomplete_resultstrue: the search timed out and you got a partial set. Rerun it.incomplete_resultsfalse with zero items: practitioners have reported this on queries that usually return hundreds. Rerun it and compare against a narrower query before you believe the zero.- 202 from a stats endpoint: the numbers aren't computed yet. Retry later and never store a 202 body as an empty result.
Dated breaks: the days GitHub data changed for good
A dated break is a day after which a source records less, or records it differently, and never restores the earlier data. Any series or comparison that spans one of these dates needs a flag before it reaches anyone.
The break dates, in order:
- 2023-11-29: GitHub announces the 10,000-commit stats change.
- 2024-12-03: a 24-hour test of 30-day event retention.
- 2025-01-30: event retention is cut from 90 days to 30.
- 2025-01-31: in the same changelog, Enterprise users with 31 or more days of inactivity start showing as dormant.
- 2025-05-24: GH Archive loses about 40% of events. The break log uses the 2025-05-23 15:00 file, at 262,958 events, as its May reference point.
- 2025-08-08, 2025-09-08, 2025-10-07: the Events payload trim is announced, browned out (switched off temporarily as a test), and goes live.
- 2025-10-08 to 16: GH Archive loses payloads. The 2025-10-10 12:00 file held 881 events, against 166k on 2025-10-08.
- Spring 2026: GH Archive loses non-push events.
- September and October 2026: GH Archive loses pushes. Since 2026-10-27, hourly files hold about 3k events.
The summary in the CNCF-linked break log is blunt: every one of these drops was permanent. By the August 2026 samples, PullRequestEvents had fallen from about 8% of all archive events to under 1%. One analysis found GH Archive captured 14% fewer events in 2025 than in 2024 and estimates that it retains only about 50% of events.
What the payload trim removed
GitHub said PR and push payloads would be trimmed, IDs would be kept so details could be fetched from REST, and push events would lose commit summaries and counts. Practitioners observed that the PushEvent commits array, author name and email, and author_association were all removed. All of this shipped under the same 2022-11-28 API version header, so a version pin protects nothing.
One user's capture on 2026-10-08 showed what is left:
- PushEvent:
before,head,push_id,ref,repository_id. - PullRequestEvent:
id,number,url,base,head.
You can rebuild two things: a compare link from before...head, and commit details by calling the commits endpoint on the head SHA. The trim moved the cost from GitHub onto the consumer, because every push now needs its own REST call to get its commits. Harness SEI was missing non-PR commits from 2025-10-07 to 2026-01-05, roughly three months, before backfilling through REST.
How to label a repo-derived value, step by step
The procedure has seven steps. It runs from an inventory of fields to a labelled, cross-checked, compliance-cleared export, and every step ends in something you can check. Run it once per pipeline, then again whenever a source or a break changes.
From raw field to labelled value
- Inventory every repo-derived fieldList each field with its source endpoint and pull date. Done when every field maps to exactly one source.
- Attach the window and cap for each sourceEvents 30 days or 300 events, search 1,000, contributors 500 emails, stats 10,000 commits. Done when the lookup table has no blank limit cells.
- Log the truncation signals on every pullStore total_count against items returned, incomplete_results, 422, 202, anon rows, and all-zero additions. Done when each pull keeps its raw status code and flags.
- Check the span against the dated breaksCompare each span with 2025-01-30, 2025-05-24, 2025-10-07, 2025-10-08 to 16, and 2026-10-27 onward. Done when every field that spans a break carries a flag.
- Label each valueAssign complete, capped, windowed, or broken, choosing the most severe when several apply. Done when no zero is left without a label.
- Cross-check against a complete routeUse git clone and log, GraphQL history totalCount, date-sliced search, or REST refetch by ID. Done when each label is confirmed or overturned.
- Run the compliance gateStrip commit emails and other personal fields before anything goes to outreach. Done when the export holds no personal fields from commits.
Owners and rough effort, as planning estimates rather than measured times:
- Steps 1 and 2: the ops owner. About half a day, then about an hour.
- Step 3: a data engineer. One to two days, because it changes the ingestion code.
- Steps 4 and 5: an analyst. About an hour for the break check.
- Step 6: a data engineer. Hours to days, depending on the rate budget.
- Step 7: ops or legal.
Labelling comes down to two questions: is the value at a cap or past a window, and does its span cross a dated break?
Labelling a value from two questions
Record every audited value as one row, so the label and its evidence travel together:
field_name: source_endpoint: pulled_at_utc: span_start: span_end: window_or_cap: total_count: items_returned: incomplete_results: status_code: anon_rows: all_zero_additions_deletions: breaks_crossed: label: complete | capped | windowed | broken cross_check_route: cross_check_result: confirmed | overturned | pending checked_by:
One row per field per pull; keep the raw signals even when the label is complete.
Step 6 is where most teams stall, because GraphQL backfills and archive pipelines are specialist work. If nobody in-house has done it, Refolk will find people who have from a plain-English request. That beats guessing at titles in a boolean string.
How repo-derived data goes wrong: failure modes and false positives
Each failure mode below is a value that looks complete, gets shipped, and then turns into a wrong shortlist, a wrong "dormant" label, or a wrong momentum chart. Each entry gives what the bad value looks like, what is actually true, and the check that catches it.
1. "No activity" flag built from Events
- Looks like: an empty event list, so the person gets marked dormant.
- Actually: anything older than 30 days returns nothing, and a repo with more than 300 recent events pushes older ones out. A typical false positive is a maintainer who shipped 45 days ago.
- Check: the commits endpoint with
since, or git log.
"Dormant" now effectively means 31 days, because GitHub's Enterprise dormancy rule and the Events window share the same 30-day horizon. Any events-based dormancy flag inherits that window.
2. Search counts that stop at 1,000
- Looks like: a list of 1,000 PRs or repos treated as the whole set.
- Actually:
total_countreads 1,100 or more and the rest were never returned. - Check: if
total_countis greater than the number of items, split by created-date slices until every slice is under 1,000.
3. Zero items with incomplete_results false
- Looks like: a clean, flagged-complete empty result.
- Actually: this has been reported on queries that normally return hundreds.
- Check: rerun the query and compare it with a narrower one.
4. Contributor tables that undercount or double count
- Looks like: a contributor roster or head count.
- Actually: people after email 500 show up as anonymous, and people with several emails are counted twice.
- Check: request
anon=1rows, then dedupe on git log authors.
5. Zero lines of code on a large repo
- Looks like: a contributor who added and deleted nothing.
- Actually: the 10,000-commit stats cap. Commit counts come through, but additions and deletions are zeroed.
- Check: if the commit count is above 0 while every addition and deletion is 0, and history
totalCountis at or above 10,000, the value is capped.
6. Push commit count of 0 after 2025-10-07
- Looks like: pushes with no commits. One abuse detector read these as clean users.
- Actually: the payload trim removed the commits array.
- Check: if
beforediffers fromhead, commits exist. Call compare onbefore...head.
7. Momentum charts built from GH Archive
- Looks like: declining stars and PRs, so the project is losing steam.
- Actually: an artifact of the source, not a change in user behaviour. CNCF DevStats traced a sharp drop in Kubernetes contributors above its 50-contribution threshold to GH Archive.
- Check: does the series cross May 2025 or October 2025? If it does, label it broken.
8. Archive gaps that look like rewritten history
- Looks like: a force-push, because a push's
beforeSHA never appears in the archive. - Actually: a missing archive hour, where the archive never logged the push that created that SHA. The World of Code commit-provenance paper excluded 440,594 pushes that straddled an outage for this reason.
- Check: see whether the missing SHA's hour has an abnormally small file, then confirm against a clone.
9. Outreach emails taken from commits
- Looks like: a ready-made contact list.
- Actually: GitHub's Acceptable Use Policies block this whatever the collection route. See the compliance section below.
The checks above as ordered rules, ready to drop into a labelling job:
IF status_code = 202 -> NOT COMPUTED: retry later, never store IF field uses push commits, commit counts, or author fields AND any event date >= 2025-10-07 -> BROKEN (rebuild via compare before...head or commits on head SHA) IF source = GH Archive AND span crosses 2025-05-24, 2025-10-08..16, spring 2026, or 2026-10-27 onward -> BROKEN IF status_code = 422 -> CAPPED IF total_count > items_returned -> CAPPED (split by created-date slices) IF incomplete_results = true -> CAPPED (partial after timeout; rerun) IF incomplete_results = false AND items_returned = 0 -> RERUN and compare with a narrower query before labelling IF anon_rows > 0 -> CAPPED (dedupe on git log authors) IF commits > 0 AND all additions = 0 AND all deletions = 0 -> CAPPED (10,000-commit stats) IF source = Events AND span_start < pulled_at minus 30 days -> WINDOWED IF source = Events AND events_returned = 300 -> WINDOWED ELSE -> COMPLETE
Replace field names with your pipeline's columns; keep the order so broken outranks capped and capped outranks windowed.
What does a complete cross-check cost?
Every complete route costs API requests or clone time, and the rate limits decide how fast you can turn capped and windowed values into confirmed ones. Budget the cross-check before you promise a delivery date.
GitHub's published limits:
- Primary: 60 requests per hour unauthenticated, 5,000 authenticated, and 15,000 for Enterprise Cloud organization apps.
- Secondary: 900 points per minute for REST and 2,000 for GraphQL, with at most 100 concurrent requests.
| Route | Max per call | Calls for full cap | Share of 5,000/hr |
|---|---|---|---|
| Events (old docs: 30 per page) | 30 | 10 for 300 | 0.2% |
| Search | 100 | 10 for 1,000 | 0.2% |
| GH Archive hour, 2025-10-08 vs 10-10 | 166k vs 881 events | n/a | -99.5% (derived) |
Reaching a cap is cheap. Getting past it is the expensive part. The Events per-page figure comes from the old docs and is unverified for the current API. The request cost of a git clone isn't publicly established, so measure it on your own repos.
Complete route by source:
| Source | Complete route |
|---|---|
| Events API | Commits endpoint with since; git log |
| Search | Created-date slices; incremental GraphQL sync for PRs |
| Contributors | Git log authors, deduped across emails |
| Repository stats | git log --shortstat --no-merges; GraphQL history totalCount |
| Trimmed payloads | REST refetch by ID; compare before...head |
Practitioners disagree about how to fix archive gaps. Harness backfilled through REST. CNCF DevStats backfills from the API and from git clones. A GH Archive issue (#190) says the archive "can't stand in for REST ingestion". An OpenDigger archive compatible with GH Archive has been live since 2026-09-06. Before swapping one archive for another, run the same hourly-count comparison on it that you would run on GH Archive. Where commit truth matters, I'd use clones, because git history is the only layer in the stack with no window and no cap.
What do GitHub's Acceptable Use Policies allow you to do with this data?
Collecting data through the API isn't scraping under GitHub's policy, but every collection route carries the same restrictions on how you use the data. The policy says "Scraping does not refer to the collection of information through our API", and it then bans certain uses whatever the route.
The restrictions that matter for recruiting and sales pipelines:
- No spam from any route: you may not use information "whether scraped, collected through our API, or obtained otherwise" for unsolicited email.
- No selling to recruiters: the same clause covers selling personal information to recruiters.
- Purpose limit: personal information may only be used for the purpose the user authorized.
This is step 7 of the procedure, and it runs even when every value is labelled complete. A correct value can still be one you aren't allowed to use. Shortlists built on repo signals should reach people through channels they chose to publish, not through addresses lifted from commit metadata.
How do you keep this reference current?
Re-check the lookup table on a schedule, because GitHub has changed what these sources return without bumping the API version. Watch the mechanism, not just the current values: changelog announcements, the GH Archive break log, and your own hourly counts.
What to watch:
- GitHub's changelog: every break in this guide was announced there first, sometimes with a brownout test date you can watch for.
- The GH Archive break log (gharchive.org issue #326): it records each volume and payload drop with example hours.
- Your own hourly event counts: a sudden drop in file size against nearby hours is the only signal the archive gives.
- Sampled payload fields: a field that goes missing under the same version header is a break even if nobody announced it.
When the person who should own this audit isn't obvious, ask Refolk for revenue operations managers at developer-tools companies who use GitHub activity for lead scoring. Seeing who holds that role elsewhere tells you which team should own it in yours.
Before a repo-derived value ships
- Every repo-derived field in the export names exactly one source endpoint and a pull date.
- Every field has its source's window or cap written beside it.
- Each pull stored total_count, items returned, incomplete_results, and the raw status code.
- No 202 response body was stored as an empty result.
- Every value carries one label: complete, capped, windowed, or broken.
- Every field whose span crosses 2025-01-30, 2025-05-24, 2025-10-07, 2025-10-08 to 16, or 2026-10-27 onward is flagged.
- Every capped, windowed, or broken value has a cross-check result of confirmed or overturned, not pending.
- No dormancy flag rests on Events data alone.
- No momentum chart built from GH Archive crosses May or October 2025 without a visible flag.
- The export contains no commit emails or other personal fields from commits.
Questions practitioners ask
Why is my GitHub activity data incomplete?
Usually a source limit has cut it off, and most of those limits return a normal 200 response. The Events API keeps only 300 events from the past 30 days. Search stops at 1,000 results. The contributors list links only 500 emails. Stats zero out at 10,000 commits. GH Archive has permanently lost volume and payload fields since 2025. Find your field's source in the lookup table, then run the cross-check listed for that row.
How do I get GitHub events older than 30 days?
You can't get them from the Events API. Since 2025-01-30 it returns only events from the past 30 days, up to 300 of them, and older events are left out even when fewer than 300 come back. For commit activity, query the commits endpoint with a since date or clone the repository and read git log. GH Archive holds older hourly files, but check whether your span crosses one of its dated breaks first.
What is the workaround for the GitHub search API 1000 results limit?
Split the query by created-date slices until each slice returns fewer than 1,000 results, then merge the slices. You know you need to split when total_count is larger than the number of items you got back. Requesting a page past result 1,000 returns a 422. For pull request history specifically, practitioners recommend an incremental GraphQL sync instead of search.
Why does the GitHub contributors API show anonymous contributors after 500?
GitHub links only the first 500 author emails in a repository to user accounts. Contributors after that, and authors whose emails do not match an account, appear as anonymous rows, which you only see when you request anon=1. The list is also cached and can be a few hours old. For a complete roster, take authors from git log and dedupe people who committed under more than one email.
Why does the GitHub stats API return 422 or zero additions for big repos?
At 10,000 or more commits, code_frequency returns 422 and stats/contributors returns 200 with commit counts but zero additions and deletions. GitHub announced this on 2023-11-29. A 202 means the stats are still being calculated, so retry it rather than store it. For line counts on large repos, GitHub's changelog points to git log --shortstat --no-merges.
Is GH Archive still reliable for push commits?
No, not for any span that touches its breaks. Push payloads lost their commits array on 2025-10-07, the archive lost payloads from 2025-10-08 to 16, and hourly files have held about 3k events since 2026-10-27. No source I found shows those losses were backfilled. Use REST refetch by ID or git clones for commit detail, and flag any series that crosses May or October 2025.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.