Efficient Notion API Queries: Combining Nested Filters with Cursor Pagination
Learn how to reduce payload size and avoid rate limits by using Notion's nested filter objects and cursor-based pagination in your integration code.
19 Feb 2026, 16:28 UTC

When building custom engineering tools that sync data with Notion, fetching entire databases and filtering locally wastes bandwidth, increases latency, and quickly triggers rate‑limit errors. The solution is to push the filtering work onto Notion’s servers and retrieve only the matching rows using cursor‑based pagination.
Server‑Side Filtering with Nested Logic
The POST /databases/{database_id}/query endpoint accepts a filter object that can combine AND and OR gates. By defining the filter in the request body, the API returns only pages that match the criteria, shrinking the payload and eliminating unnecessary client‑side processing.
{
"filter": {
"and": [
{
"property": "Status",
"select": { "equals": "Bug" }
},
{
"or": {
"children": [
{
"property": "Priority",
"select": { "equals": "High" }
},
{
"property": "Team",
"multi_select": { "contains": "Infrastructure" }
},
{
"property": "Team",
"multi_select": { "contains": "Backend" }
}
]
}
}
]
}
}
Your integration token must have at least Can view permission on the target database; otherwise the query returns a 404 even if the ID is correct.
Iterative Pagination with start_cursor
Notion limits each query to 100 results. To retrieve larger sets, inspect the has_more boolean and, when true, pass the returned next_cursor as the start_cursor in the next request. This token‑based approach avoids the pitfalls of offset‑based pagination.
async function fetchAllPages(databaseId, filterObject) {
let cursor = null;
const allPages = [];
do {
const response = await fetch(`https://api.notion.com/v1/databases/${databaseId}/query`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NOTION_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
filter: filterObject,
start_cursor: cursor,
page_size: 100
})
});
if (!response.ok) {
throw new Error(`Notion API error: ${response.status}`);
}
const data = await response.json();
allPages.push(...data.results);
cursor = data.next_cursor;
} while (data.has_more);
return allPages;
}
Worked Example: Fetching High‑Priority Bugs
Suppose you need every bug that is either high priority or assigned to the Infrastructure or Backend teams. Combine the filter from the first section with the pagination helper:
const bugFilter = {
and: [
{ property: "Status", select: { equals: "Bug" } },
{
or: {
children: [
{ property: "Priority", select: { equals: "High" } },
{ property: "Team", multi_select: { contains: "Infrastructure" } },
{ property: "Team", multi_select: { contains: "Backend" } }
]
}
}
]
};
(async () => {
try {
const bugs = await fetchAllPages('YOUR_DATABASE_ID', bugFilter);
console.log(`Found ${bugs.length} matching bugs`);
// Process bugs …
} catch (err) {
console.error(err);
}
})();
Trade‑offs and Limitations
- Rollup and Formula latency: These properties are computed asynchronously; a recent update to a relation may not be reflected in the API response for several seconds.
- Rate limits: Each plan tier enforces a request quota; bursty polling can yield 429 responses. Implement exponential back‑off or respect the
Retry‑Afterheader. - Permission granularity: The integration token must be explicitly granted access to the database (or its parent page). Missing rights produce 404 errors that can be mistaken for a wrong ID.
- Complex formulas: Formulas that reference many relations across thousands of rows can degrade database performance and increase query latency.
Actionable Closing
To build performant Notion integrations:
- Define precise
filterobjects usingAND/ORto offload work to the API. - Wrap queries in a pagination loop that respects
has_moreandnext_cursor. - Test with a database containing >200 items to verify that
next_cursorchains correctly and that no data is skipped or duplicated. - Monitor response headers for rate‑limit warnings and add retry logic.
- Validate that your integration token has the required
Can view(orCan edit) permission on the target database.
By keeping filtering and pagination on the server side, you reduce payload size, stay within API limits, and deliver faster, more reliable engineering tools.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.