GitHub rebuilt its star history API from 2,511 requests to 24 by filtering before grouping
On 14 October 2026, GitHub published the engineering story behind its reworked stargazers endpoint, which took 2,511 requests to rebuild react/react’s curve and was being harvested for spam. The lesson transfers to any API: a smaller response doesn’t make a cheaper query — the order of work in the database does.
14 October 2026. GitHub publishes a technical deep dive on the rework of its GET /repos/{owner}/{repo}/stargazers endpoint. 2,511 requests. That’s what it once took to rebuild the growth curve of react/react and its 251,000 stars. 24 requests. That’s what it costs today. Why it matters: the story applies to any team that exposes an API — the old design returned identities where users wanted trends, and it made the database do useless work on every call.
Identities where curves were needed
The original endpoint answered one question: who starred this repository? It returned a paginated list of people — at most 100 per page — and, with a custom media type, the date each star was created. The data model was built for the same job: it records the current set of people who have starred a repository, which keeps showing a count or a list simple. It was never designed as a historical event log.
When tools began reading that list as a time series, rebuilding a growth curve meant retrieving individual identities and processing every current star. The work grew with the repository’s popularity: for react/react, about 2,511 requests, so tools often hit rate limits or sampled pages instead of reading them all.
Two problems followed, at both ends. Identities were available in bulk, and bad actors began scraping public stargazer lists for spam and phishing campaigns. And on highly starred repositories, the endpoint could make the database process many thousands of entries to return a single page.
The real cost was in the database, not the response
The fix seemed obvious: return counts instead of people. But returning less data didn’t make the query cheaper. Because of a bug in the pagination implementation, every request counted all of a repository’s stars to work out how many pages existed, even though the caller received only one page of up to 100 people. On a highly starred repository, that meant counting thousands of entries on every request.
The numbers are stark. In the week before access was restricted, nearly 37,000 of these count queries ran slowly enough to be logged, averaging almost 3 seconds each, and database safeguards killed more than 1,000 of them — directly contributing to request timeouts. After the restriction, the database served about 60% fewer stargazer requests, and total time spent on the count query fell by about 80%.
GitHub therefore first restricted stargazer and watcher lists to repository administrators and collaborators. On the security side, that closed the harvesting path. On the tooling side, it broke the tools that rebuilt the curve from the old response.
The approaches ruled out
The first idea was to keep one record per star but strip the identity — return starred_at without the stargazer for callers lacking collaborator access. It was the fastest path and closed the harvesting with a light change to serialization. But consumers would still have to page through thousands of records to build an aggregate history — and the new need stayed bolted to an endpoint and pagination model designed for listing people, not reading trends.
Aggregation seemed the obvious next step. Instead of one row per star, the query would group stars into daily or weekly buckets and paginate a much smaller result. The problem was where pagination happened. In a naive SQL query, the database first finds all stars, groups them into buckets, and sorts them — only then applies LIMIT and OFFSET. For a popular repository, an anonymous request would still scan and aggregate thousands of index entries to return a handful of buckets. For react/react, reading the full history that way would have meant examining roughly 6 million rows, because each of the 24 requests would have reprocessed every star. Shrinking the response didn’t shrink the cost: the work had to be reduced before aggregation.
Filtering before grouping
The shipped design paginates star history by calendar week. Each request translates page and per_page into an indexed created_at range, filters the stars to those weeks, then groups the remaining records into daily counts. A response covers up to 30 weeks, and increasing the page moves backward through the repository’s history.
The order of operations changes everything. With the date range applied first, each request processes only the weeks being returned rather than every current star. For tools, one request now covers up to 30 weeks of history, regardless of how many stars the repository has. For react/react, that’s about 24 requests instead of 2,511, and the database examines each star roughly once — about 251,000 rows across the full history. The response shape mirrors the weekly commit activity endpoint, GET /repos/{owner}/{repo}/stats/commit_activity, aligning the API with an existing convention.
Two stated limits remain. The time range bounds each request to a defined period, but not the number of stars within it: an unusually dense week can still produce a slow query. And the comparison work surfaced other places where GitHub counted stars on demand — the stargazers page on github.com, which used the same counting, now reads the cached star count, and its weekly slow queries dropped to zero.
The rewrite didn’t happen in a vacuum. When the access restriction broke the popular Star History tool, its users rallied around the project, and GitHub’s community team connected the maintainers with product engineering, who worked alongside them to design the purpose-built endpoint. A breaking change turned into a collaboration that produced a better default for everyone.
Verdict
It’s an API architecture lesson that transfers to any exposed service. If you return a list that consumers reduce into an aggregate themselves, look first at what your database does behind it: a LIMIT doesn’t reduce the grouping and sorting already performed, it only trims the response. If your data contains identities the client never uses, remove them — anything beyond the need is data someone else can misuse. If you must paginate a history, filter on an indexed range before grouping, and align the response shape with an existing convention. The star curve of react/react no longer takes 2,511 requests to rebuild: it fits in 24, and the difference came from the order of work in the database, not the size of the response.