Blog · 2026-10-02

USAspending API Pagination: The 10,000-Record Wall

USAspending API Pagination: The 10,000-Record Wall
hasNext says done at 10,000. The data keeps going to 50,000.

Your USAspending query returns 10,000 awards. You assume that is all of them. It is not, and the API will not tell you.

The USAspending API has a pagination bug on its main award search: page_metadata.hasNext flips to false at exactly 10,000 records, even though deeper pages keep returning real, distinct results with an HTTP 200. Any client that paginates "until hasNext is false" stops at 10,000 and silently drops the rest. Here is how to detect it, and the three ways to get the full set.

Key takeaway: The USAspending API's spending_by_award endpoint has a pagination bug: page_metadata.hasNext flips to false at record 10,000 while deeper pages keep returning real rows, HTTP 200 throughout (filed as issue #4793). Clients that loop on hasNext silently truncate. Fix: ignore the flag, page until a short page (to the real 50,000 ceiling), then use the bulk-download endpoint. Edgrapi's /v1/awards now derives has_next correctly.

I reproduced this live on October 2, 2026. It is not theoretical, and it is already filed as a bug.

Why does the USAspending API stop at 10,000 records?

It does not actually stop, it lies about stopping. On the spending_by_award endpoint, page_metadata.hasNext turns false once you have paged through 10,000 records, regardless of your page size. The data past 10,000 is still there and still served, but the one flag you would use to keep looping now says there is nothing more. So well-behaved pagination code quits early with no error.

This is filed as issue #4793 on the official USAspending API repo. It is a known defect, not a misunderstanding.

The number is fixed at records, not pages. At 100 per page it breaks on page 100. At 50 per page it breaks on page 200. At 10 per page it breaks on page 1000. Always record 10,000.

That detail matters, because shrinking your page size does not help. You hit the same wall, just after more requests.

The hasNext flag goes false while real rows continue
page 100 reports hasNext false at record 10,000, but pages 101 to 500 each still return 100 real rows.

Is the USAspending hasNext flag reliable?

No. Past 10,000 records the hasNext flag is actively wrong, and nothing in the response warns you. When I paged an unbounded contracts query at 100 per page, page 100 returned a full 100 results with hasNext: false, and pages 101, 150, 200 and 500 each returned another 100 real, distinct awards, also with hasNext: false. The flag said "done" five hundred pages before the data ran out.

Here is the part that makes it dangerous: the status code is 200 the whole time.

No error. No warning. No X-Truncated header. If you trust the flag, you lose 80% or more of a large result set and your code looks like it worked.

I checked that the extra rows were genuine. Page 101's top award was ID W564KV24F0096 at $47.87M, page 102's was 75N99024C00090 at $47.33M. Distinct awards, still sorted by amount, exactly where they should be.

So the rows are not duplicates or garbage. They are the real tail of your query, hidden behind a false flag.

How can you reproduce the USAspending 10,000 bug?

Run one unauthenticated POST and watch hasNext at page 100. Hit the spending_by_award endpoint with a broad filter at 100 records per page, request page 100, then page 101. Page 100 returns hasNext: false. Page 101 returns another 100 real awards. No key, no setup, about thirty seconds, and you have seen the whole bug.

Here is the call.

curl -s -X POST https://api.usaspending.gov/api/v2/search/spending_by_award/ \
  -H "Content-Type: application/json" \
  -d '{"filters":{"award_type_codes":["A","B","C","D"],
       "time_period":[{"start_date":"2024-10-01","end_date":"2025-09-30"}]},
       "fields":["Award ID","Recipient Name","Award Amount"],
       "sort":"Award Amount","order":"desc","limit":100,"page":101}'

Page 101 comes back with a full 100 results and hasNext: false. Bump page to 150 or 500 and it keeps paying out.

The USAspending API needs no key at all, so anyone can confirm this in a terminal right now. Do that before you trust a total built on this endpoint.

How do I know if my USAspending result is truncated?

Look at the cursor fields in page_metadata, not just hasNext. When pagination breaks at 10,000, USAspending returns last_record_unique_id: null and last_record_sort_value: "None" alongside hasNext: false. Those two fields go null exactly when the flag lies. A null cursor on a full page is the tell that you are at the broken wall, not the real end.

Compare two things on every final-looking page.

Did the page come back full? If you asked for 100 and got 100, there is almost certainly more, whatever hasNext says.

Are the cursor fields populated? A real last page returns a usable last_record_sort_value. The broken wall returns "None".

If the page is full and the cursor is null, you are being truncated. Keep going.

The cheapest guard is an assertion. If your pull ends on a full page, or lands on exactly 10,000, log it loud and treat the result as incomplete. A result that ends on a round 10,000 is almost never a coincidence with this endpoint; it is the fingerprint of the bug. Teams get burned here precisely because the failure is quiet, so the fix is to make it noisy in your own code even when the API stays silent.

Four USAspending pagination approaches compared
Trust hasNext (broken, 10k), page until short (~50k), search-after cursor (~50k), or bulk download (~500k).

How do I get more than 10,000 results from USAspending?

You have three honest options, and the right one depends on how many records you need. For up to about 50,000, ignore hasNext and page until a page comes back short. For deep, stable pulls, use the search-after cursor instead of page numbers. For a full extract or anything above 50,000, skip paging entirely and use the bulk download endpoint. Each has a hard ceiling you should know before you start.

Here is the trade-off in one place.

ApproachHow it worksCeilingUse when
Trust hasNextLoop while the flag is true10,000 (broken)Never, for large sets
Page until shortIgnore hasNext, stop on a page under your limit~50,000Up to 50k records
Search-after cursorPass last_record_sort_value forward~50,000, more stable at depthDeep paging, less duplication
Bulk downloadPOST a filter, get a CSV/ZIP file~500,000 per jobFull extracts, big pulls

The one rule that ties them together: stop trusting hasNext on anything that might exceed 10,000 rows.

Page-until-short is the simplest fix and the one most people need. You ask for a full page, and a full page back means there is more, no matter what the flag says. The first page that returns fewer rows than you asked for is the genuine end. It costs you one extra request at the tail, which is cheap.

The search-after cursor is the more correct tool for deep pulls. Instead of asking for "page 300," you pass the previous page's last_record_sort_value and last_record_unique_id forward, and the API returns the next slice relative to that record. It avoids the drift and duplication that plain offset paging suffers at depth, because the database is not re-counting from zero every request. Use it when you are pulling tens of thousands of rows and want each one exactly once.

Both still bottom out at the 50,000 page ceiling. That is not a bug, it is a deliberate bound on expensive deep queries, and it is where bulk download takes over.

What is the real USAspending pagination ceiling?

Page-number pagination caps at page * limit <= 50,000. Past that, USAspending's offset paging stops returning data and starts erroring, because deep offset paging is expensive and they bound it. So "page until short" recovers records 10,001 through roughly 50,000, then genuinely ends. In my test, pages kept returning full results to page 500 (record 50,000) and then broke above it.

That 50,000 line is the real limit of page-based access, not the 10,000 the flag implies.

If your query returns more than 50,000 awards, no amount of clever paging fixes it. The answer is either a narrower query or a bulk download.

Narrowing is underrated. Filter by a single agency, a NAICS code, or a shorter date range, and most queries drop under 50,000 on their own, which keeps you in fast paging territory. A query scoped to one agency and one quarter almost never approaches the ceiling at all.

How do I bulk download USAspending data?

Use the download endpoint, which builds a file instead of paging. POST your filters to /api/v2/download/awards/ and USAspending queues a job, then returns a status URL and, when ready, a link to a CSV or ZIP with every field. A single job handles up to roughly 500,000 records, far past what paging reaches, and it carries the complete column set rather than the trimmed fields a search returns.

The trade-off is that it is asynchronous. You submit, you poll a status URL, you download when it is ready, and big jobs can take minutes.

One gotcha: the bulk download usually requires an agency filter. An unbounded "everything" job tends to fail or time out, so scope it.

The flow is submit, poll, download. You POST the filter and get back a status_url and a file_url. You poll the status until the job reports finished, then pull the file. For a job of any size, build in a timeout and a retry, because a large unbounded request can run for minutes and occasionally fails outright and has to be resubmitted.

What you get is the full column set, not the handful of fields a search returns, which is the other reason to prefer it for analysis. A search gives you the fields you asked for; the download gives you everything USAspending has on each award.

For a weekly full refresh of a slice of federal spending, this is the correct tool. For a live lookup of the top awards in a category, it is overkill, and paging is faster.

How do I paginate spending_by_award correctly?

Page by full-page detection, not by the flag. Request 100 per page, keep going as long as each page returns a full 100 rows, and stop the first time a page returns fewer, or when page * limit reaches 50,000. Treat hasNext as unreliable above 10,000 and never use it as your only stop condition. For anything larger than 50,000, switch to bulk download.

In practice that is a short loop.

page, limit, out = 1, 100, []
while page * limit <= 50000:
    rows = post_spending_by_award(filters, page=page, limit=limit)["results"]
    out += rows
    if len(rows) < limit:        # a short page is the real end
        break
    page += 1
# more than 50k rows? use /api/v2/download/awards/ instead

Notice there is no hasNext in that loop. That is the point. A full page means keep going; a short page means stop.

If you would rather not own this logic, a normalized wrapper can own it for you. That is the case for using one.

How edgrapi fixes the pagination truncation
edgrapi derives has_next from a full page, recovering the hidden 10k-50k records and ending cleanly.

Does edgrapi's awards API have the same 10,000 limit?

Not anymore. Edgrapi's /v1/awards wraps the same USAspending data, and it used to inherit the bug because it trusted hasNext directly. We fixed that on October 2, 2026: has_next is now derived from whether a page came back full, capped at USAspending's real 50,000 ceiling. So it returns has_next: true through record 49,900 and a clean has_next: false at 50,000, instead of lying at 10,000.

That means you can page /v1/awards and actually get the 10,001st through 50,000th record, which the raw flag hides.

I want to be straight about the limit: edgrapi recovers the 10k-to-50k band and ends cleanly. It does not magically exceed USAspending's 50,000 page ceiling. Past that, the honest answer is still a bulk download, and the docs say so.

Under the hood the fix is small and the same one you would write yourself: has_next comes from whether the page was full, not from the upstream flag, with the ceiling applied so it stops at 50,000 rather than erroring past it. The point is not that it is clever. The point is that you do not have to remember any of this every time you pull awards.

The reason this matters for a data API is correctness. The data is public and needs no key, so the only thing a wrapper owes you is getting the boring parts right, and silent truncation is the least boring thing to get wrong. A pagination flag that lies is exactly the kind of defect that never shows up in a demo and quietly corrupts a production report for months.

Check your last page before you trust your numbers

If you pull from USAspending and have ever seen exactly 10,000 results, assume you were truncated and re-run it. Ignore hasNext, page until a short page or 50,000, then bulk download above that. If you would rather not maintain pagination that fights the API, edgrapi's /v1/awards returns correct has_next on the same federal data, free to start. Count your last page before you report a total.

Frequently asked questions

Why does the USAspending API stop at 10,000 records?

It does not stop, it mislabels. On spending_by_award, page_metadata.hasNext turns false at record 10,000 regardless of page size, while deeper pages keep returning real results with an HTTP 200. It is a known bug, filed as issue #4793 on the official USAspending repo. Clients that loop on hasNext silently truncate at 10,000.

Is the USAspending hasNext flag reliable?

No. Above 10,000 records it is actively wrong. In a live test on October 2, 2026, pages 101 through 500 each returned 100 distinct, correctly sorted awards while hasNext read false the whole way, with a 200 status and no warning. Do not use hasNext as your only stop condition on any query that might exceed 10,000 rows.

How do I get all results past 10,000 from the USAspending API?

Ignore hasNext and page until a page returns fewer rows than your limit, which works up to about 50,000 records. For deeper or more stable pulls, use the last_record_sort_value search-after cursor. For full extracts or anything above 50,000, POST your filters to the bulk download endpoint instead of paging.

What is the USAspending API pagination limit?

Two limits. The flag breaks at 10,000 records, but that is a bug, not the real cap. Page-number pagination actually works up to page * limit <= 50,000; past that, offset paging errors out. So page-based access tops out near 50,000 records, and the bulk download endpoint handles up to roughly 500,000 per job.

How do I bulk download <a href="/blog/government-contract-api">federal spending data</a>?

POST your filters to /api/v2/download/awards/. USAspending queues a job and returns a status URL, then a link to a CSV or ZIP with the full column set, up to about 500,000 records per job. It is asynchronous, so you poll until it is ready. It usually requires an agency filter, so scope the query rather than requesting everything.

Does edgrapi's awards API have the same 10,000 limit?

No longer. Edgrapi's /v1/awards wraps USAspending and once inherited the bug, but as of October 2, 2026 it derives has_next from a full page and caps at the real 50,000 ceiling. It recovers the 10,001st through 50,000th records the raw flag hides and ends cleanly. Above 50,000, it points you to the bulk download, same as the raw API.

Get a free API key