Beginner4 min read

Pagination in DynamoDB

DynamoDB never returns "all" results in one call. A Query or Scan returns at most 1 MB of data, then hands you a LastEvaluatedKey to resume from. Getting pagination right means looping on that key — not on a counter.

The loop

let key;
do {
  const out = await client.send(new QueryCommand({...params, ExclusiveStartKey: key}));
  process(out.Items);
  key = out.LastEvaluatedKey;
} while (key);

When LastEvaluatedKey is undefined, you've reached the end. Pass it back as ExclusiveStartKey to fetch the next slice.

The control flow is a single loop that exits only on an absent key:

presentabsentQuery / ScanProcess ItemsLastEvaluatedKey?Set ExclusiveStartKeyDone

Every pass either resumes from the returned key or stops — there is no counter.

Limit is not a page size

Limit caps how many items DynamoDB evaluates, not how many it returns after a FilterExpression. A Limit: 25 query behind a filter can return 3 items and still hand you a LastEvaluatedKey — you must keep paging until the key is empty, even when a page looks short. A non-empty LastEvaluatedKey never promises more matching items either; only an absent key proves you've reached the end.

Let the SDK paginate

Both SDKs wrap the loop above so you can iterate pages directly:

// AWS SDK for JavaScript v3
import {paginateQuery} from '@aws-sdk/lib-dynamodb';
for await (const page of paginateQuery({client}, params)) {
  process(page.Items);
}
# boto3
paginator = client.get_paginator('query')
for page in paginator.paginate(**params):
    process(page['Items'])

No page numbers

DynamoDB has no total count and no random page access — you can't jump to "page 7" or page backwards without replaying the cursors. Design UIs around infinite scroll / "load more", not numbered pages. (A Select: 'COUNT' query still reads — and bills for — every matched item to count them.)

Stateless cursors for APIs

LastEvaluatedKey is just the key attributes of the last item. Base64-encode it and hand it to clients as an opaque nextToken; decode it back into ExclusiveStartKey on the next request. No server-side cursor state.

That token is DynamoDB-JSON — eyeball or hand-craft one with the DynamoDB-JSON converter. And if you're paging to work around a Scan, that's usually a signal to add an index instead.

Try DynoTable to page through query results visually, with the cursor tracked for you.

Updated