Engineering a Notion Content Sync Using Block‑Based Retrieval
Learn how to recursively fetch Notion blocks, handle pagination, and avoid N+1 queries when building a reliable content synchronization pipeline.
20 Feb 2026, 15:22 UTC

Problem: Syncing Nested Notion Content Without Losing Structure
When you try to mirror a Notion page in another system, each piece of content—text, image, table, toggle—is stored as a separate block with its own ID. The API returns only the top‑level blocks unless you ask for their children, which means a naïve sync can miss nested content or generate many redundant requests.
Solution: Recursive Block Retrieval with Pagination
The Notion API provides a Retrieve block children endpoint that accepts a block ID and returns its immediate children, along with a next_cursor for pagination. By calling this endpoint recursively for every block that has children, you can reconstruct the full tree while respecting pagination limits.
Worked Example: Fetching a Page Containing a Table and a Toggle List
// Pseudocode (JavaScript‑like)
async function fetchAllBlocks(blockId) {
const stack = [{ id: blockId, depth: 0 }];
const result = [];
while (stack.length) {
const { id, depth } = stack.pop();
let cursor = null;
do {
const resp = await notion.blocks.children.list({
block_id: id,
start_cursor: cursor,
page_size: 100,
});
for (const child of resp.results) {
result.push({ ...child, depth });
if (child.has_children) {
stack.push({ id: child.id, depth: depth + 1 });
}
}
cursor = resp.next_cursor;
} while (cursor);
}
return result;
}
// Call with the page’s root block ID
// const blocks = await fetchAllBlocks('page_root_id');
This example uses a depth‑first stack to avoid deep recursion limits, paginates each block’s children with a page size of 100, and collects every block with its nesting depth.
Trade‑off: Rate Limiting and N+1 Queries
Each call to children.list counts toward your integration’s rate limit (average three requests per second). Deeply nested pages can trigger many requests, resembling an N+1 problem. To stay within limits, batch requests where possible, implement exponential backoff on 429 responses, and consider caching block IDs that rarely change.
Actionable Checklist
- Start with the page’s root block ID (obtainable via
pages.retrieve). - Use the recursive fetch pattern shown above, respecting
next_cursor. - Monitor HTTP 429 responses and apply a back‑off delay (e.g., 500 ms, then 1 s, then 2 s).
- Verify the sync by comparing the total block count returned with the count shown in Notion’s UI for the same page.
- If you need to update content, target the specific block ID returned from the fetch rather than replacing the whole page body.