Traditional offset-based pagination (using SKIP and TAKE) isn't viable in DynamoDB due to performance constraints. Instead, DynamoDB uses cursor-based pagination through LastEvaluatedKey, which acts as a pointer to the next page.
While navigating "Next" is straightforward, implementing a full set of controls—First, Previous, Next, and Last—requires a deeper understanding of DynamoDB's architecture.
In this guide, we’ll implement a complete pagination solution in C#.
Introduction to DynamoDB Pagination
Amazon DynamoDB is a fully managed NoSQL database designed for fast, scalable, and predictable performance. When querying large datasets, DynamoDB automatically paginates results and returns up to 1 MB of data per request.
Instead of using offset-based pagination like SQL, DynamoDB uses a special value called LastEvaluatedKey. Each query response includes:
-
A page of items.
-
A
LastEvaluatedKey(if more items exist).
To retrieve the next page, the client passes this key back to DynamoDB using the ExclusiveStartKey parameter. Because DynamoDB does not support random access to pages, implementing controls like Previous and Last requires applying cursor logic or manipulating the sort order.
When to Use Pagination in DynamoDB
Pagination is essential when dealing with:
-
Large datasets: Fetching thousands of items in a single request is inefficient and costly.
-
User interfaces: UI components (dashboards, tables) need friendly controls.
-
APIs returning limited result sets: Public endpoints must paginate to avoid timeouts.
-
Reducing Read Costs: Controlled queries reduce Read Capacity Units (RCU) consumption.
-
High-traffic systems: Fetching data incrementally prevents backend resource exhaustion.
The Secret Weapon: ScanIndexForward
DynamoDB allows you to navigate forward easily. However, it does not natively support "Previous" or "Last". To solve this, we utilize the ScanIndexForward parameter.
-
ScanIndexForward = true(Default): Returns items in ascending order. -
ScanIndexForward = false: Returns items in descending order.
This feature allows us to:
Efficiently get the "Last Page"
Querying in descending order gives you the newest/last items first.
Key Concept: The first page of a descending query is effectively the last page of an ascending query.
Support Backward Pagination
When moving backward, using reverse sort order allows us to fetch items preceding the current batch without scanning the entire table.
Note: This technique requires your Table or GSI to have a Sort Key defined.
Implementing Pagination (C# Example)
Below is a reusable pagination structure supporting First, Next, Previous, and Last.

Important: Handling State in Web APIs
Before looking at the code, note that in a stateless environment (like a REST API), you cannot store the PagingState object in server memory. You must serialize the state (e.g., to a Base64 JSON string) and send it to the client. The client must then send this token back in the next request.
Models
using Amazon.DynamoDBv2.Model;
// Represents the result of a paginated query
public class PageResult
{
// List of DynamoDB items returned for this page
public List<Dictionary<string, AttributeValue>> Items { get; set; }
// Cursor pointing to the next page (null if no more pages)
public Dictionary<string, AttributeValue>? NextKey { get; set; }
// Number of items per page
public int PageSize { get; set; }
}
// Stores the pagination state for navigating Next/Previous
public class PagingState
{
// Stack of previous page tokens -> used to move backwards
// In a Web API, this list should be serialized and sent to the client
public Stack<Dictionary<string, AttributeValue>?> PrevTokens { get; set; } = new();
// Token used to load the current page
public Dictionary<string, AttributeValue>? CurrentToken { get; set; }
// Token used to load the next page
public Dictionary<string, AttributeValue>? NextToken { get; set; }
}
Base Query Method
This generic method handles the core DynamoDB query logic.
public async Task<PageResult> QueryPageAsync(
string userType,
Dictionary<string, AttributeValue>? startKey,
int pageSize,
bool scanForward = true)
{
var request = new QueryRequest
{
TableName = "Users",
// Partition key condition
KeyConditionExpression = "UserType = :u",
ExpressionAttributeValues = new Dictionary<string, AttributeValue>
{
{":u", new AttributeValue { S = userType }}
},
// Cursor for next page (null for first page)
ExclusiveStartKey = startKey,
// Maximum items to return
Limit = pageSize,
// Sorting direction: true = ascending, false = descending
ScanIndexForward = scanForward
};
var response = await _client.QueryAsync(request);
return new PageResult
{
Items = response.Items,
NextKey = response.LastEvaluatedKey,
PageSize = pageSize
};
}
First Page
public async Task<PageResult> GetFirstPageAsync(string userType, int pageSize, PagingState state)
{
// Clear backward history as we are starting over
state.PrevTokens.Clear();
state.CurrentToken = null;
// Load first page in ascending order
var result = await QueryPageAsync(userType, null, pageSize, scanForward: true);
// Store next page cursor
state.NextToken = result.NextKey;
return result;
}
Next Page
public async Task<PageResult> GetNextPageAsync(string userType, int pageSize, PagingState state)
{
// Check if there are more pages
if (state.NextToken == null)
return new PageResult { Items = new(), NextKey = null };
// Save current token to history so we can navigate backwards later
state.PrevTokens.Push(state.CurrentToken);
// Move forward
state.CurrentToken = state.NextToken;
// Load next page
var result = await QueryPageAsync(userType, state.CurrentToken, pageSize);
state.NextToken = result.NextKey;
return result;
}
Previous Page
public async Task<PageResult> GetPreviousPageAsync(string userType, int pageSize, PagingState state)
{
// If no history, default to First Page
if (!state.PrevTokens.Any())
return await GetFirstPageAsync(userType, pageSize, state);
// Retrieve the most recent previous token
var previousKey = state.PrevTokens.Pop();
// Update current cursor
state.CurrentToken = previousKey;
// Load page using the retrieved token
var result = await QueryPageAsync(userType, previousKey, pageSize);
state.NextToken = result.NextKey;
return result;
}
Last Page
This is where the magic happens using ScanIndexForward = false.
public async Task<PageResult> GetLastPageAsync(string userType, int pageSize)
{
// Reverse the sort order so newest items come first
// This effectively fetches the "Last Page" immediately
var result = await QueryPageAsync(
userType,
startKey: null,
pageSize: pageSize,
scanForward: false); // Critical: Read backwards
// Reorder items for UI display (so they appear Ascending within the page)
result.Items.Reverse();
return result;
}
Note on Navigation: Jumping directly to the "Last Page" isolates the user from the previous navigation history. The
PrevTokensstack will not automatically know how to go back to the "Second to Last" page. In most UI implementations, clicking "Last" resets the navigation context.
Conclusion
DynamoDB’s cursor-based pagination offers a scalable and cost-efficient alternative to offset-based pagination. While paging forward is simple, paging backward and jumping to the last page requires creative use of sorting.
By leveraging ScanIndexForward = false, developers can:
-
Retrieve the last page instantly (O(1) complexity).
-
Reverse the paging direction efficiently.
-
Reduce unnecessary read costs.
With the C# implementation provided, you now have a robust starting point for building user-friendly tables on top of DynamoDB.
References
These authoritative resources help deepen your understanding:
-
AWS Docs – Query API
https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html -
AWS Docs – DynamoDB Pagination
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.Pagination.html -
AWS .NET SDK Documentation
https://docs.aws.amazon.com/sdkfornet/v3/apidocs/index.html -
Best Practices for DynamoDB
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html
Ready to get started?
Contact IVC for a free consultation and discover how we can help your business grow online.
Contact IVC for a Free Consultation









