
The Actions API Now Stops Counting At 2,500
Quick answer: Two GitHub Actions API changes shipped 24 hours apart. From 25 September 2026, filtered workflow-run queries report "2,500+" instead of an exact total_count. From 24 September, expired artifacts are no longer returned by the REST API or shown in run summaries. Neither breaks a build. Both quietly break scripts that treat those numbers as facts.
If you have a dashboard that reports "failed runs this month" or a cleanup job that walks artifacts, it is now returning different answers than it did last week, and nothing in your logs will say so.
These are the changes nobody announces twice, because neither is a breaking change in the strict sense. They are worse than that: they are silent semantic changes to values your code already trusted.
Change one: counts become approximate
GitHub's 25 September changelog entry describes the new behaviour for list workflow runs for a repository as "a less precise but more accurate count of records when you search by workflow, event, status, branch, or actor." Past 2,500 matching records, you get 2,500+ rather than a number. The stated reason is honest enough: those queries frequently timed out and returned incomplete counts, so an admitted ceiling beats a wrong integer.
It rolled out immediately on github.com and GitHub Enterprise Cloud. There is no flag and no opt-out window.
Now put it next to what the REST reference already said. That endpoint documents per_page with a maximum of 100 and a default of 30, and states: "This endpoint will return up to 1,000 results for each search when using the following parameters: actor, branch, check_suite_id, created, event, head_sha, status" (REST API: workflow runs).
The two ceilings do not line up
Read those together and the shape follows. On a filtered query you can page through at most 1,000 records, while the count is only capped at 2,500. GitHub's wording does not spell out#39;s wording does not spell out the sub-2,500 behaviour, but the implication is that between 1,000 and 2,500 the API hands you a number of runs it will not let you enumerate, and above 2,500 it stops claiming a number at all.
| Matching runs | total_count | Records you can page |
|---|---|---|
| Under 1,000 | Exact | All of them |
| 1,000–2,500 | Exact | 1,000 |
| Over 2,500 | 2,500+ | 1,000 |
If your reporting ever relied on total_count for a busy repository, it was already reading a number it could not reproduce by counting. The change makes that visible rather than introducing it.
Change two: expired artifacts disappear
The 24 September entry is shorter and has sharper edges: "Expired artifacts are no longer displayed in the GitHub Actions run summary or returned by the REST API." Two endpoints are named — list artifacts for a repository and get an artifact. Previously expired artifacts stayed listed with an "Expired" marker even though their bytes were long gone, which led people to assume they were still being billed for them.
GitHub is explicit that retention settings, retention periods and billing are unchanged. Only visibility changed. Artifact details produced by a run are still recoverable from that run's logs.
Worth noting as a documentation lag: the REST reference for artifacts still documents the expired boolean and expires_at timestamp in its response schema and says nothing about exclusion. If you go by the reference alone you will still expect "expired": true to be a state you can list. It is now a state you can only ever observe in transit.
What actually breaks in practice
None of this fails loudly. That is the problem. The failure modes:
Metrics that parse total_count. A monthly failure-rate chart on a busy monorepo silently flattens at 2,500 and stays there. Your numbers stop moving and the graph looks stable, which is the worst possible presentation of a broken metric.
Cleanup jobs. A script that lists artifacts and deletes expired ones now finds none and reports success. Nothing was wrong; nothing was done either.
Inventories and audits. Anything that reconstructs "what did this pipeline produce in Q3" from the artifacts API now sees only what is still within retention.
Storage forecasting. If you were counting listed artifacts as a proxy for consumption, the proxy just got better — expired entries never consumed storage — but your historical series is not comparable across the change.
The fixes are unglamorous. Derive counts by narrowing with the created parameter into windows small enough to enumerate, then sum the windows — a week at a time is usually under the ceiling even on noisy repositories. Record artifact names, IDs and sizes into your own store at upload time, in the same step that produces them, rather than reading them back later. And when you are eyeballing a response to work out which fields you are actually getting, a JSON formatter beats piping through jq from memory. If you want the shape as a type instead of a guess, JSON to TypeScript generates it from the real payload.
How to tell in five minutes whether you are affected
Run the two queries by hand against a busy repository. Request workflow runs filtered by status=failure and look at what total_count now contains — if it is a capped value, anything downstream that does arithmetic on it is already wrong. Then list artifacts for a repository you know had expired ones last month and count the results. Two requests, and you know which of your scripts need attention. Comparing the response you get today against a stored response from August is faster still, if you kept one.
Retention is the setting that actually matters
Since expired artifacts are now invisible, the retention period is the only lever that determines what you can still see. The defaults are worth knowing precisely: checks, workflow runs, commit statuses, artifacts and logs are retained for 90 days. Public repositories can be configured between 1 and 90 days; private repositories between 1 and 400. A changed retention period applies only to newly generated objects, not to what already exists.
Included artifact storage sits alongside that: 500 MB on GitHub Free, 1 GB on Pro, 2 GB on Team, 50 GB on GitHub Enterprise Cloud. Shortening retention is the cheapest way to stay inside the included amount, and now it is also the setting that decides how much of your own build history you are able to query.
The broader pattern is familiar from the rest of this month's Actions changes — defaults and platform behaviour moving underneath workflows nobody edited, as with the pull_request_target default block and the ubuntu-latest move to Ubuntu 26.04. Read the changelog weekly; your CI has more unpinned dependencies than your lockfile does. If you are looking for tooling to put around it, the cloud and DevOps category is the relevant shelf.
FAQ
Why does total_count say 2,500+?
Because from 25 September 2026 the list-workflow-runs endpoint reports a capped count when a filtered query matches more than 2,500 records. GitHub made the change because larger counted queries frequently timed out and returned incomplete numbers.
How do I count more than 2,500 workflow runs?
Split the query by date with the created parameter and sum the windows. Keep each window under the ceiling, and remember that filtered queries page through at most 1,000 records regardless.
Are expired artifacts deleted or just hidden?
Their files were always deleted at expiry; what changed is that the records are no longer listed in the UI or returned by the REST API. Retention periods and billing are unaffected.
Can I still find out which artifacts a run produced?
Yes, from the run's logs, which still record the artifacts the run uploaded. Anything you need programmatically after expiry should be written to your own store at upload time.
What are the artifact retention limits?
90 days by default, configurable from 1 to 90 days for public repositories and 1 to 400 days for private ones. New values apply only to objects generated after the change.
An API that admits it is guessing is an improvement. Your dashboard still needs to hear about it.


