Query Pagination and Continuation
Complete the full lesson to earn 25 points — 50 with Pro
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks, the wait, and see fewer ads — read each lesson on a single page with Pro
Lesson: Mastering Query Pagination and Continuation
Introduction: Why Pagination Matters
In the world of modern application development, we are rarely dealing with static, small datasets. Whether you are building a social media feed, a financial reporting dashboard, or an inventory management system, your data will inevitably grow to a size that cannot—and should not—be returned in a single network request. When you query a database, the system must allocate memory, process the request, and serialize the data into a response format like JSON. If you attempt to fetch one million records at once, you will likely crash your server, exhaust your database's connection pool, or cause a timeout that leaves the user staring at a loading spinner indefinitely.
Query pagination is the architectural practice of breaking a large result set into smaller, manageable chunks called "pages." By implementing effective pagination, you ensure that your application remains responsive, memory-efficient, and cost-effective. Furthermore, pagination is a critical aspect of user experience (UX) design. Users rarely need to see ten thousand items at once; they prefer to consume information in smaller, digestible segments. Mastering pagination is not just about keeping your system alive; it is about creating a predictable, performant interface that behaves consistently regardless of how much data resides in your backend.
This lesson explores the mechanics of pagination, the differences between various implementation strategies, and how to handle "continuation" tokens—the hidden engine behind modern, high-performance data retrieval.
Understanding the Core Concepts of Pagination
At its simplest level, pagination is about defining a boundary for a query. Without a boundary, a query is "unbounded," meaning it will attempt to return every single record that matches the criteria. To prevent this, we introduce two primary parameters: limit (the maximum number of items per page) and offset (the starting point of the retrieval).
Offset-Based Pagination
Offset-based pagination is the most common approach. It works by telling the database to skip a certain number of rows and then return the next N rows. For example, if you want page 3 with a page size of 20, you set the limit to 20 and the offset to 40 (20 items * 2 previous pages).
While intuitive, offset-based pagination has a major drawback: performance degradation. Databases typically have to scan all the skipped rows before returning the requested ones. If you have a table with millions of rows and you request page 50,000, the database must perform a significant amount of work just to throw away the first 999,980 rows. Additionally, this method is prone to "data drift." If a new item is inserted while a user is navigating between pages, the same item might appear on two different pages, or an item might be skipped entirely.
Cursor-Based (Continuation) Pagination
Cursor-based pagination, often referred to as continuation tokens, solves the performance and consistency issues of offset-based systems. Instead of telling the database "skip X rows," you tell it "start after this specific item." The "cursor" acts as a pointer to the last item retrieved in the previous set. Because the database can use an index to jump directly to the record identified by the cursor, the query performance remains constant, regardless of how deep into the dataset you are.
Callout: Offset vs. Cursor Pagination
- Offset Pagination: Simple to implement, works well for small datasets, allows random access (jumping to page 50). However, it is slow on large datasets and suffers from inconsistent results if the data changes frequently.
- Cursor Pagination: Highly performant, provides consistent results even with concurrent updates, and is ideal for infinite scrolling. However, it does not allow random access (you must follow the chain of pointers) and is slightly more complex to implement.
Implementing Pagination with SDKs
Most modern SDKs for cloud databases (like Azure Cosmos DB, AWS DynamoDB, or MongoDB) provide built-in mechanisms to handle continuation. Let’s look at how this works in a practical scenario using a hypothetical document database SDK.
Step-by-Step: Implementing Continuation Tokens
When you execute a query, the response object typically contains a collection of items and a property called a continuationToken (or paginationToken). This token is an opaque string that contains the internal state required for the database to know where to resume the next query.
- Initial Request: You issue a query with a
MaxItemCountdefined. You do not provide a continuation token. - Process Response: The SDK returns a page of results and a
ContinuationToken. - Client Storage: You save this token, usually sending it back to the client or storing it in the session state.
- Subsequent Request: When the user requests the next page, you execute the same query, but you pass the stored
ContinuationTokeninto the SDK’s request options. - Iteration: Repeat this until the token returns null, indicating there is no more data.
Code Example: Using Continuation Tokens
// Example using a generalized document store SDK
async function fetchAllData(container, querySpec) {
let continuationToken = null;
let allResults = [];
do {
// Execute the query with the current token
const { resources, continuationToken: nextToken } = await container.items.query(querySpec, {
continuationToken: continuationToken,
maxItemCount: 100
}).fetchAll();
allResults.push(...resources);
continuationToken = nextToken;
// Continue until the token is null
} while (continuationToken);
return allResults;
}
In this code snippet, we use a do-while loop. The key is that the continuationToken is updated in every iteration. The SDK handles the heavy lifting of decoding the token and applying the correct filters to the query execution plan.
Note: Never try to parse or modify the contents of a continuation token. It is an implementation detail of the database engine and can change its format or encoding at any time without notice. Treat it as a completely opaque string.
Best Practices for Data Pagination
When designing systems that utilize pagination, adhering to industry standards ensures that your API is predictable and your infrastructure remains healthy.
1. Define Reasonable Page Sizes
While it is tempting to allow clients to request as many items as they want, this is a dangerous practice. Always enforce a maximum page size (e.g., 100 or 200 items). If a client requests a page size of 10,000, your server should either reject the request or cap it at your defined maximum. This prevents "Denial of Service" scenarios where a malicious or poorly written client consumes all your memory.
2. Standardize Your API Response
Consistency is key for the developers consuming your API. Your response object should always follow a predictable structure. A standard response might look like this:
{
"data": [...],
"meta": {
"pageSize": 50,
"continuationToken": "eyJhbGciOiJIUzI1Ni..."
}
}
By separating the data from the meta information, you make it easier for client-side libraries to automatically detect pagination logic and handle the tokens without cluttering the business data.
3. Handle Empty Results Gracefully
A common pitfall is failing to handle the "empty" state. If your query returns zero results, your SDK might return an empty array and a null or missing continuation token. Your code should explicitly check for this and return a clean response to the user, rather than throwing an error or attempting to process an undefined token.
4. Use Consistent Sorting
For cursor-based pagination to work reliably, the sort order must be deterministic. If you query items by createdAt without a secondary sort key, and two items share the exact same timestamp, the database might return them in a different order on subsequent pages. Always include a tie-breaker, such as a unique ID, in your ORDER BY clause.
Warning: The "No Sort" Trap
If you do not explicitly order your results, the database engine does not guarantee the order of return. This means that if you use cursor-based pagination on an unsorted query, you might see the same item multiple times or miss items entirely because the "next" page is being calculated against a result set that is effectively shuffled.
Comparing Strategies
To help you decide which strategy is right for your use case, refer to the following comparison table:
| Feature | Offset-Based | Cursor-Based |
|---|---|---|
| Performance | Decreases as offset increases | Constant (O(1) lookup) |
| Random Access | Yes (e.g., jump to page 10) | No (Sequential) |
| Consistency | Low (susceptible to drift) | High (stable) |
| Complexity | Low | Medium |
| Best For | Small lists, UI with page numbers | Infinite scrolling, large datasets |
Common Pitfalls and How to Avoid Them
The "Off-by-One" Error
In offset pagination, developers often confuse the index with the count. If you are on page 1 (using 0-based indexing), your offset is 0. If you are on page 2, your offset is pageSize. A common mistake is to calculate the offset as page * pageSize instead of (page - 1) * pageSize. Always double-check your math, or better yet, use a library that handles the calculation for you.
Leaking Implementation Details
Sometimes, developers expose the raw database query or the raw continuation token format directly to the public. This is a security risk. If your continuation token contains internal document IDs or database-specific metadata, you might be leaking information about your underlying data structure. If you are concerned about security, consider base64-encoding your continuation tokens or using a wrapper that obscures the internal state.
Ignoring Timeouts
When processing large datasets, you might be tempted to fetch everything at once. Even with pagination, if the individual requests take too long, the overall process might time out. If you are performing a background job to export data, ensure that your pagination logic is integrated with a retry mechanism and that your timeouts are configured to account for the total duration of the operation, not just the single request duration.
Lack of State Management
On the client side, managing the state of a paginated list can get messy. If a user triggers multiple requests in rapid succession, you might receive responses out of order. Implement a "loading" state and disable the "Next" button while a request is in flight. Furthermore, consider using a library for state management (like React Query or TanStack Query) that handles caching and pagination state automatically.
Deep Dive: The Mechanics of Continuation Tokens
To truly understand why continuation tokens are superior for large-scale applications, we must look at how the database engine handles them. When you query a database, the engine creates a "query cursor." This cursor holds the internal state of the scan, including the filters applied, the index currently being traversed, and the point at which the scan stopped.
When you finish a batch, the database doesn't necessarily want to keep that cursor open indefinitely, as it consumes memory on the server. Instead, it serializes the necessary metadata into the continuation token and sends it to the client. When you send that token back, the database deserializes it, "rehydrates" the cursor, and continues the scan from exactly where it left off.
This is fundamentally different from offset pagination, where the database must re-run the query and re-calculate the position every single time. With a continuation token, the database engine is essentially performing a "resume" operation rather than a "restart" operation.
Advanced Usage: Bidirectional Pagination
Some applications require users to move both forward and backward through data. While simple cursor pagination is unidirectional, you can implement bidirectional pagination by storing the cursor for the start of the current page and the cursor for the end of the current page.
To go forward:
- Use the "end" cursor.
- The database fetches the next N items.
- Generate a new "end" cursor.
To go backward:
- Use the "start" cursor.
- The database fetches the previous N items (by reversing the sort order).
- Generate a new "start" cursor.
This is significantly more complex to implement but provides a seamless user experience for applications that require "Previous/Next" navigation.
Best Practices for API Design
When you are exposing these pagination mechanisms via an API, you should follow RESTful principles.
- Use Query Parameters: Use standard query parameters like
?limit=50&cursor=XYZrather than putting these values in the request body. This makes the URLs shareable and cacheable. - Use Headers for Metadata: If you have extensive metadata (e.g., total estimated count, next link, previous link), consider using the
Linkheader as defined in the RFC 5988 standard. This keeps your response body clean and focused on the actual data. - Provide HATEOAS Links: If possible, include full URLs in your response for the next and previous pages. This allows the client to navigate your API without needing to know how to construct the query parameters themselves.
Example: RESTful Response with HATEOAS
{
"items": [...],
"_links": {
"next": "/api/v1/orders?limit=50&cursor=abc123def",
"self": "/api/v1/orders?limit=50&cursor=xyz789"
}
}
By providing the full URL for the next page, you decouple the client from your internal URL structure. If you decide to change the way pagination is handled in the future, you only need to update the logic that generates these links, and the client-side code will continue to function without changes.
Troubleshooting Common Issues
1. The "Duplicate Item" Problem
If you see the same item appearing on both page 1 and page 2, it is almost certainly because your sort order is not deterministic.
- The Fix: Ensure your
ORDER BYclause includes a unique field, such as a primary key or a timestamp with a secondary unique ID.
2. The "Missing Item" Problem
If items are disappearing, it is often due to the data changing while you are paginating. If an item is updated and its sort key changes (e.g., a "last updated" timestamp), it might move from a page you already processed to a page you have yet to process.
- The Fix: Use a "snapshot" approach if absolute consistency is required, or accept that in highly dynamic systems, pagination is "eventually consistent." If the data must be perfectly consistent, you might need to use a versioning system to lock the view of the data.
3. The "Invalid Token" Error
Sometimes, a continuation token becomes invalid (e.g., if the underlying index was rebuilt or the data was deleted).
- The Fix: Your API should be prepared to handle an error from the database when an invalid token is provided. In such cases, the best practice is to return a 400 Bad Request error or to restart the query from the beginning, informing the user that the previous session has expired.
The Role of SDKs and Abstraction Layers
While it is important to understand the mechanics, you should lean on the abstraction layers provided by your SDK. Most enterprise-grade SDKs, such as the Azure Cosmos DB .NET SDK or the MongoDB Node.js driver, have built-in support for asynchronous iteration.
For example, in modern JavaScript/TypeScript, you can use for await...of loops with certain SDKs to iterate through pages seamlessly:
// Using an async iterator (if supported by your SDK)
for await (const page of container.items.query(querySpec).getAsyncIterator()) {
for (const item of page.resources) {
console.log(item.id);
}
}
This syntax hides the continuation token entirely. The SDK handles the token management, the loop logic, and the HTTP requests behind the scenes. This is the "gold standard" for developer productivity. Always check your SDK documentation to see if it supports async iterators or similar high-level abstractions before writing your own manual pagination loops.
Security Considerations
Pagination can be used as an attack vector. A common technique is "resource exhaustion" via pagination. If an attacker requests a very large page size across many concurrent requests, they can overwhelm your database.
- Rate Limiting: Implement rate limiting on your API endpoints.
- Input Validation: Strictly validate the
limitparameter. Do not allow it to be negative, and do not allow it to exceed a hard-coded maximum. - Query Complexity Analysis: Some databases allow you to limit the "cost" of a query. Ensure that your queries are indexed appropriately so that the database doesn't have to perform a full collection scan to satisfy the pagination request.
Conclusion: Key Takeaways
As you wrap up this module, keep these core principles in mind to ensure your data models remain performant and reliable:
- Always Paginate: Never return an unbounded list of data. Even if you think your dataset will remain small, it will eventually grow. Building pagination from day one is much easier than refactoring it later.
- Prefer Cursor-Based Pagination: For most production systems, especially those with large datasets or high concurrency, cursor-based pagination is superior to offset-based pagination. It provides constant performance and better data consistency.
- Ensure Deterministic Sorting: Your pagination is only as reliable as your sort order. Always include a unique, non-changing identifier in your sort criteria to avoid data drift and duplicate items.
- Treat Tokens as Opaque: Never attempt to parse, modify, or store the contents of a continuation token. Treat it as a black box provided by the SDK.
- Design for the Client: Use clear API standards like the
Linkheader or consistent JSON meta-objects to make your API easy for front-end developers to consume. - Handle State and Errors: Plan for the "empty" state, the "invalid token" state, and the "loading" state. A robust application manages these states gracefully to provide a professional user experience.
- Leverage SDK Abstractions: Whenever possible, use built-in SDK features like async iterators to handle pagination. This reduces your code surface area and minimizes the risk of implementing custom pagination logic incorrectly.
By mastering these concepts, you transition from simply "fetching data" to "architecting data delivery." This distinction is what separates junior developers from those capable of building high-scale, reliable systems that stand the test of time. Implement these patterns, respect the limits of your infrastructure, and always prioritize the consistency and performance of the user experience.
Reach the last section to complete this lesson and earn points — you're on section 1 of 10.
- Introduction to Cosmos DB Data Modeling
- Introduction to Cosmos DB Data Modeling Quiz5q
- Multiple Entity Types in Same Container
- Multiple Entity Types in Same Container Quiz5q
- Storing Related Entities in Same Document
- Storing Related Entities in Same Document Quiz5q
- Denormalizing Data Across Documents
- Denormalizing Data Across Documents Quiz5q
- Referencing Between Documents
- Referencing Between Documents Quiz5q
- Partition Keys and Document IDs
- Partition Keys and Document IDs Quiz5q
- Time to Live (TTL) Configuration
- Time to Live (TTL) Configuration Quiz5q
- Document Versioning Strategies
- Document Versioning Strategies Quiz5q
- Schema Versioning Patterns
- Schema Versioning Patterns Quiz5q
- Choosing Partition Strategies
- Choosing Partition Strategies Quiz5q
- Partition Key Selection Best Practices
- Partition Key Selection Best Practices Quiz5q
- Transactions and Partition Keys
- Transactions and Partition Keys Quiz5q
- Cross-Partition Query Costs
- Cross-Partition Query Costs Quiz5q
- Data Distribution Analysis
- Data Distribution Analysis Quiz5q
- Throughput Distribution Planning
- Throughput Distribution Planning Quiz5q
- Synthetic Partition Keys
- Synthetic Partition Keys Quiz5q
- Hierarchical Partition Keys
- Hierarchical Partition Keys Quiz5q
- Throughput and Storage Requirements
- Throughput and Storage Requirements Quiz5q
- Serverless vs Provisioned Throughput
- Serverless vs Provisioned Throughput Quiz5q
- Database-Level Provisioned Throughput
- Database-Level Provisioned Throughput Quiz5q
- Granular Scale Units
- Granular Scale Units Quiz5q
- Global Distribution Costs
- Global Distribution Costs Quiz5q
- Configuring Throughput in Portal
- Configuring Throughput in Portal Quiz5q
- Gateway vs Direct Connectivity Mode
- Gateway vs Direct Connectivity Mode Quiz5q
- Creating Database Connections
- Creating Database Connections Quiz5q
- Azure Cosmos DB Emulator
- Azure Cosmos DB Emulator Quiz5q
- Connection Error Handling
- Connection Error Handling Quiz5q
- Singleton Pattern for Clients
- Singleton Pattern for Clients Quiz5q
- Global Distribution Regions
- Global Distribution Regions Quiz5q
- Threading and Parallelism
- Threading and Parallelism Quiz5q
- Arrays and Nested Objects Queries
- Arrays and Nested Objects Queries Quiz5q
- Correlated Subqueries
- Correlated Subqueries Quiz5q
- Array and Type-Checking Functions
- Array and Type-Checking Functions Quiz5q
- Mathematical and String Functions
- Mathematical and String Functions Quiz5q
- Date Functions in Queries
- Date Functions in Queries Quiz5q
- Point Operations vs Query Operations
- Point Operations vs Query Operations Quiz5q
- CRUD Point Operations
- CRUD Point Operations Quiz5q
- Patch Operations for Updates
- Patch Operations for Updates Quiz5q
- Transactional Batch Operations
- Transactional Batch Operations Quiz5q
- Bulk Operations with SDK
- Bulk Operations with SDK Quiz5q
- Optimistic Concurrency with ETags
- Optimistic Concurrency with ETags Quiz5q
- Query Pagination and Continuation
- Query Pagination and Continuation Quiz5q
- Cosmos DB Mirroring for Fabric
- Cosmos DB Mirroring for Fabric Quiz5q
- Mirroring vs Spark Connector
- Mirroring vs Spark Connector Quiz5q
- Enabling Analytical Store
- Enabling Analytical Store Quiz5q
- Synapse Spark and SQL Queries
- Synapse Spark and SQL Queries Quiz5q
- Change Data Capture in Analytical Store
- Change Data Capture in Analytical Store Quiz5q
- Azure Functions and Event Hubs Integration
- Azure Functions and Event Hubs Integration Quiz5q
- Denormalization with Change Feed
- Denormalization with Change Feed Quiz5q
- Referential Integrity with Change Feed
- Referential Integrity with Change Feed Quiz5q
- Azure AI Search Integration
- Azure AI Search Integration Quiz5q
- Azure Functions Change Feed Trigger
- Azure Functions Change Feed Trigger Quiz5q
- Consuming Change Feed with SDK
- Consuming Change Feed with SDK Quiz5q
- Change Feed Estimator
- Change Feed Estimator Quiz5q
- Denormalization via Change Feed
- Denormalization via Change Feed Quiz5q
- Aggregation Persistence with Change Feed
- Aggregation Persistence with Change Feed Quiz5q
- Read-Heavy vs Write-Heavy Indexing
- Read-Heavy vs Write-Heavy Indexing Quiz5q
- Index Type Selection
- Index Type Selection Quiz5q
- Custom Indexing Policies
- Custom Indexing Policies Quiz5q
- Composite Index Implementation
- Composite Index Implementation Quiz5q
- Index Performance Optimization
- Index Performance Optimization Quiz5q
- Response Status Codes and Metrics
- Response Status Codes and Metrics Quiz5q
- Normalized RU Consumption Monitoring
- Normalized RU Consumption Monitoring Quiz5q
- Server-Side Latency Metrics
- Server-Side Latency Metrics Quiz5q
- Data Replication Monitoring
- Data Replication Monitoring Quiz5q
- Azure Monitor Alerts Configuration
- Azure Monitor Alerts Configuration Quiz5q
- Resource Logs Implementation
- Resource Logs Implementation Quiz5q
- Partition Throughput Monitoring
- Partition Throughput Monitoring Quiz5q
- Encryption Key Management
- Encryption Key Management Quiz5q
- Network-Level Access Control
- Network-Level Access Control Quiz5q
- Data Encryption Configuration
- Data Encryption Configuration Quiz5q
- Azure RBAC for Control Plane
- Azure RBAC for Control Plane Quiz5q
- Microsoft Entra ID for Data Plane
- Microsoft Entra ID for Data Plane Quiz5q
- CORS Settings Configuration
- CORS Settings Configuration Quiz5q
- Customer-Managed Keys
- Customer-Managed Keys Quiz5q
- Always Encrypted Implementation
- Always Encrypted Implementation Quiz5q
- Data Movement Strategy Selection
- Data Movement Strategy Selection Quiz5q
- SDK Bulk Operations for Data Movement
- SDK Bulk Operations for Data Movement Quiz5q
- Azure Data Factory Pipelines
- Azure Data Factory Pipelines Quiz5q
- Kafka Connector Integration
- Kafka Connector Integration Quiz5q
- Azure Stream Analytics Integration
- Azure Stream Analytics Integration Quiz5q
- Cosmos DB Spark Connector
- Cosmos DB Spark Connector Quiz5q
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles the points you earn on every lesson and quiz so you progress twice as fast, unlocks half of every practice exam — plus full case studies — with the Learn & Exam study modes, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× points per lesson & quiz
- ✓ 50% of every exam unlocked
- ✓ Learn & Exam modes
- ✓ Distraction-free lessons