Fix the event capture regression: poll all three pages of /events - #1
Merged
Conversation
The crawler polled only page 1 of the Events API. GitHub does not spread event
types evenly over the pages: page 1 carries the push / create / delete stream,
and everything else - pull requests, issues, comments, stars, reviews, releases,
forks - is only reachable on pages 2 and 3. Reading page 1 alone therefore
captured pushes and silently dropped nearly everything else, which is the
degradation reported in #310 and #320.
Measured on the live API: of 4654 unique events observed over 78 seconds, 1995
of 1996 PushEvents came from page 1, while all 944 PullRequestEvents, all 434
IssueCommentEvents and all 225 WatchEvents came from pages 2 and 3.
Reproduced against the live API with the previous code, 20 seconds of capture:
283 PushEvent, 13 CreateEvent, 3 DeleteEvent, 1 IssuesEvent
and with this change, 2 minutes of capture, 16248 events and no duplicates:
10894 PushEvent 436 PullRequestReviewEvent 47 ForkEvent
2054 PullRequestEvent 385 CreateEvent 33 PublicEvent
868 IssueCommentEvent 349 PullRequestReviewComment 21 CommitCommentEvent
583 IssuesEvent 328 WatchEvent 15 MemberEvent
118 ReleaseEvent 4 DiscussionEvent
110 DeleteEvent 3 GollumEvent
Changes:
- poll pages 1..3, each on its own schedule, so a busy page 1 cannot delay the
others. Page 4 is not fetched: the API refuses it with "pagination is limited
for this resource";
- de-duplicate against a bounded set of recently seen event ids. The previous
code compared only against the immediately preceding response, so an event
that briefly dropped out of the window was archived twice;
- restore the missed-records alarm. It tested `new_events.size >= PAGE_LIMIT`,
and f1f4200 set PAGE_LIMIT to 500 while the API clamps per_page to 100, so the
condition could never be true and the crawler ran saturated and silent. It is
now per page, and rate-limited to one line a minute because page 1 is expected
to saturate;
- accept several tokens separated by commas, used round-robin. The polling rate
above costs 14400 requests/hour, more than one user token allows;
- back off when the remaining quota runs behind the window, and honour
Retry-After on 403 and 429 instead of hammering through a throttle;
- use conditional GETs with per-page ETags, from #317 by @emalaj-seq;
- only report to StatHat when STATHATKEY is set, instead of raising on every
cycle;
- fix the git:// remotes in the Gemfile, also from #317. GitHub disabled the
git:// protocol in 2022, so bundle install could not resolve them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
alexey-milovidov
added a commit
that referenced
this pull request
Aug 17, 2026
Follow-up to #1. Two claims made there were wrong, and measuring properly exposed a scheduling bug. A single response from /events is an unordered sample of the recent window rather than the strict newest 100, so two adjacent responses can barely overlap while their union still covers everything. The per-poll "all 100 entries are new" check added in #1 therefore fired constantly on page 1 without any events actually being lost, and the claim that pushes "cannot be captured completely at any affordable polling rate" was drawn from that same misreading. What the aggregate actually shows, measured on page 1 over 20 second windows: interval requests/s events/s captured overlap largest timestamp gap 0.5 s 1.80 100.0 46% 1.0 s 2.0 s 0.50 50.0 0% 2.0 s 4.0 s 0.25 25.0 0% 4.0 s The stream runs at about 100 events/second and unique events scale linearly with the length of the window, so it is a complete stream and two polls a second capture all of it. Polling harder buys nothing: four concurrent workers at 7.7 requests/second found one extra event over a single worker at 1.9, with the already-seen fraction rising from 44% to 87%. More clients cannot raise the ceiling, because at this rate there is none. So the signal to watch is the overlap between polls, aggregated per page per minute: healthy is comfortably above zero, and a page at 0% for a whole minute means consecutive polls never met and the difference was dropped. Reporting that immediately turned up a bug in #1: the next poll was scheduled from the moment the response came back, so each interval was silently stretched by a round trip and page 1 ran at 0.85 requests/second instead of 2. It now schedules from the moment the request goes out. before: 103 polls/minute, overlap p1:0% p2:21% p3:94%, warning on page 1 after: 193 polls/minute, overlap p1:42% p2:57% p3:97%, no warning Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The crawler polled only page 1 of the Events API. GitHub does not spread event types evenly over the pages: page 1 carries the push / create / delete stream, and everything else — pull requests, issues, comments, stars, reviews, releases, forks — is only reachable on pages 2 and 3. Reading page 1 alone captured pushes and silently dropped nearly everything else.
This is the degradation reported upstream in igrigorik/gharchive.org#310 and #320.
Evidence
Of 4654 unique events observed on the live API over 78 seconds, grouped by the page each first appeared on:
Before and after, against the live API
Previous code, 20 seconds — 300 events, 4 types, no stars, no pull requests, no comments:
This branch, 2 minutes — 16248 events, 16 types, zero duplicates, 67% push / 33% non-push:
Star capture goes from about 0.03 to 2.7 events/second, which matches what the API actually serves.
Changes
pagination is limited for this resource, so 300 events per poll is the ceiling.new_events.size >= PAGE_LIMIT, and f1f4200 setPAGE_LIMITto 500 while the API clampsper_pageto 100 — so the condition could never be true and the crawler ran saturated and silent for 15 months. It is now per page, and rate-limited to one line a minute because page 1 is expected to saturate.Retry-Afteron 403/429 instead of hammering through a throttle.STATHATKEYis set, instead of raising on every cycle.git://remotes in the Gemfile, also from #317 — GitHub disabled that protocol in 2022, sobundle installcould not resolve them.Known limitation
Page 1 alone runs at over 190 events/second — 100 entries replaced in under 0.53 s — which is faster than the API will serve to a single client. Pushes therefore cannot be captured completely at any affordable polling rate; pages 2 and 3 can. The crawler now reports when this happens rather than hiding it.
🤖 Generated with Claude Code