has_more comes back false and the response carries a request_status that marks the result as incomplete.
This trips up full exports. A loop that just follows next_cursor until has_more is false looks like it finished, but on a 25,000-row database it quietly returns 10,000 rows. This guide shows how to detect the limit and read every row past it.
A runnable version of everything here is in the query-large-data-sources cookbook example.
Detect when a query was cut off
Every page of a query response can include arequest_status. When the result set is capped it is:
request_status.type === "incomplete" means the whole query result was capped, even if that signal appears before the last page you receive.
TypeScript
Read every row with created_time windows
Because the limit is per query, you can change the query to get a fresh budget. Split the data source into windows bycreated_time: sort ascending, and each time a window hits the limit, start a new query from the last row’s timestamp.
Page through the window
Follow
next_cursor until has_more is false. The limit also ends the window, because it sets has_more to false.created_time, not last_edited_time. created_time never changes, so windows stay stable. last_edited_time shifts as rows are edited, which moves rows between windows and causes gaps or duplicates.
TypeScript
The Notion JavaScript SDK includes
iterateAllDataSourceRows and collectAllDataSourceRows helpers that wrap this pattern, so you can stream or collect a full data source without writing the windowing loop yourself.When a window can’t be split
Notion storescreated_time to the minute. If a single minute holds more than 10,000 rows, for example from a bulk import, the window can’t advance by time alone, and the loop above throws. Add another filter to narrow each window, such as a status or category your data divides on, so every window stays under the limit.
When to use a different approach
Windowing reads the whole data source on demand. Two alternatives are often a better fit:- Incremental sync. If you poll a data source on a schedule to catch changes, switch to connection webhooks. They notify you of changes as they happen, so you never paginate the full data source or hit the limit.
- Smaller payloads. If queries are slow rather than truncated, use the
filter_propertiesparameter to return only the properties you need. See the performance recommendations on the query endpoint.
Views
The same 10,000-result limit applies to view queries, but you can’t window them. A view query paginates a fixed, already-capped result set and doesn’t accept a filter while paginating. To read every row behind a view, query its underlying data source with the approach above. Pass the view’s filter into the windowed data source query so you keep the same row set, then apply the view’s sort locally after collection if your export needs the exact display order. A retrieved view tells you which data source it is scoped to, along with the filter and sort it applies. Read those, then run the windowing query against the data source. The windowing query must keep its owncreated_time sort, so don’t pass view.sorts into queryAllRows:
TypeScript
buildWindowFilter combines the view filter with the created_time lower bound using and. If a view has a top-level or filter, the sample throws. Run one windowed query per or branch and merge rows by ID, or rewrite the view filter as an and group, so the window bound does not exceed the API’s filter nesting limit.Related
- Query a data source endpoint reference
- Pagination conventions
- query-large-data-sources cookbook example