Skip to main content
A single data source query returns at most 10,000 results. That limit is per query, not per data source: a query is defined by its filter and sort, and once one reaches the limit, pagination stops. 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 a request_status. When the result set is capped it is:
Check every response page as you paginate. Any page with 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 by created_time: sort ascending, and each time a window hits the limit, start a new query from the last row’s timestamp.
1

Sort by created_time ascending

This gives every window a stable order to walk through.
2

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.
3

If the window was incomplete, start the next one

Filter to created_time on or after the last row you saw, and repeat. Rows that share the boundary timestamp appear in both windows, so de-duplicate by row ID.
Use 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 SDK for JavaScript and TypeScript includes iterateAllDataSourceRows and collectAllDataSourceRows helpers (v5.23.0 and later) 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 stores created_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_properties parameter 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 own created_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.