This content originally appeared on Level Up Coding - Medium and was authored by Juan Andrés Leiva
.NET API Pagination Explained: Offset vs Keyset vs Cursor Strategies Using DTO
Pagination is a core part of API design in .NET. It controls how clients request large datasets, affects performance at scale, and determines how your API evolves over time.
In a previous article, I introduced pagination concepts. This post goes beyond that. We will discuss the three standard pagination strategies in .NET APIs (Offset, Keyset, Cursor), with real-world examples, plug-and-play code for your project, performance trade-offs, and advice from production systems.

Offset-Based Pagination in .NET (Skip/Take)
Offset pagination is the most common approach: page + pageSize.
For more details on how to implement this, I suggest reading my other article Minimalistic Pagination in Using a Record DTO.
Trade-offs
✅ Pros
- Simple to implement with LINQ.
- Works with “jump to page” UI.
❌ Cons
- Slow for large offsets (database scans rows).
- Can skip/duplicate results if new rows are added during pagination.
👉 Use Case: Admin dashboards, reporting tools, smaller datasets.
How to Mitigate .Skip() Performance Issues
Offset pagination slows down as offsets grow.
- Prefer keyset or cursor when datasets are large.
- If offset is required: add indexes on the sort column and project DTOs early.
Keyset Pagination in .NET (Seek Method)
Keyset pagination (a.k.a. seek method) uses a stable column like Id or CreatedAt.
var results = dbContext.Products
.Where(p => p.Id > lastSeenId)
.OrderBy(p => p.Id)
.Take(pageSize)
.ToList();
Keyset pagination avoids skipping altogether. Instead, you say: “Start after this last known value, and take the next M rows ordered by a stable column (e.g., ID or timestamp).” The database can use the index on that column to jump directly to the right spot. This makes it fast and consistent even with millions of rows. The limitation is that you can’t jump to arbitrary pages — you can only move forward (or backward if implemented explicitly).
Sample DTO for Keyset pagination:
public record KeysetPaginatedResults<T>(
IEnumerable<T> Items,
int PageSize,
int? LastId,
bool HasNextPage,
int? TotalCount = null
);
Trade-offs
✅ Pros
- Much faster on large datasets.
- Stable under inserts/deletes.
- Perfect for infinite scroll and logs.
❌ Cons
- Requires a stable sort column.
- Can’t jump to “page 7.”
👉 Use Case: Mobile feeds, activity streams, logs, event history.
Cursor-Based Pagination in .NET APIs
Cursor pagination generalizes keyset by returning an opaque cursor that encodes state.
{
"data": [...],
"nextCursor": "eyJsYXN0SWQiOjEyM30="
}
Cursor pagination is a more abstract form of keyset pagination. The API returns an opaque token (cursor) that encodes where the last query left off. Clients pass the cursor back to fetch the next batch. Internally, the database still uses keyset-style filters (WHERE Id > lastId). This makes it just as fast and stable, while hiding implementation details from clients. The main drawback is complexity — encoding/decoding cursor state and maintaining compatibility can be tricky
Sample DTO for Cursor pagination:
public record CursorPaginatedResults<T>(
IEnumerable<T> Items,
int PageSize,
string? NextCursor,
string? PreviousCursor,
bool HasNextPage,
bool HasPreviousPage,
int? TotalCount = null
);
Trade-offs
✅ Pros
• Hides internal details from clients.
• Safe for public APIs.
• Supports multi-column ordering (date + ID).
❌ Cons
• More complex to implement.
• Clients must store and pass cursors.
👉 Use Case: Public APIs, GraphQL endpoints, partner integrations.
⚙️ Hybrid Pagination Strategy
Many APIs combine strategies:
- Offset for admin dashboards.
- Keyset for user-facing apps.
- Cursor for public/partner APIs.
Pro Tip: Let the API contract drive the pagination method, not the database convenience.
Real-World Pagination Scenarios
- Admin dashboard with filters → Offset (page counts matter).
- Mobile infinite scroll → Keyset (smooth, stable).
- Partner API → Cursor (opaque, version-safe).
- Audit log export → Keyset or Cursor (large dataset iteration).
- Analytics UI → Offset (needs totals).
- Multi-tenant API → Mix based on client needs.
Common Pagination Pitfalls in .NET
- Always enforce a max page size (never let clients request 1M rows).
- Always apply .OrderBy() for consistent results.
- Use .Select() before .Skip()/.Take() to avoid materializing huge entities.
- Cache row counts for expensive queries and always make it optional.
- Keep cursors opaque so clients don’t depend on internal IDs.
- Return metadata (totalItems, totalPages, nextCursor) alongside data.
Sample Repository
Explore working examples in .NET 8:
👉 GitHub: dotnet-pagination-strategies
The repo shows:
- Offset pagination with Skip/Take.
- Keyset pagination by Id.
- Cursor pagination with Base64 state.
- Singleton dataset (no DB required).
⚠️ For experimentation only — adapt before production.
How to navigate the example
This example contains all three pagination strategies discussed. You will find the implementations within the WeatherForecastService.
public PaginatedResults<WeatherForecast> GetWeatherForecast(int page, bool getTotalCount);
public KeysetPaginatedResults<WeatherForecast> GetWeatherForecastKeyset(int? lastId = null, int pageSize = 15, bool showTotalCount = false);
public CursorPaginatedResults<WeatherForecast> GetWeatherForecastCursor(string? cursor = null, int pageSize = 15, bool showTotalCount = false);
Now the WeatherForecastController will expose three different endpoints to test each of the pagination implementations from the service.
It’s worth noting that we have sequential IDs in the example, and that allows us to create a very simple implementation. You may want to consider which field to use for these paginations
Offset-based
[HttpGet(Name = "GetWeatherForecast")]
public IActionResult Get(int? pageNumber, bool? getTotalCount)
{
var page = pageNumber ?? 0;
var result = _weatherForecastService.GetWeatherForecast(page, getTotalCount ?? false);
return Ok(result);
}
Can be invoked with something like:
http://localhost:5293/WeatherForecast?pageNumber=1&getTotalCount=true
Keyset
[HttpGet("keyset")]
public IActionResult GetKeysetPagination([FromQuery] int? lastId = null, [FromQuery] int pageSize = 15, [FromQuery] bool getTotalCount = false)
{
var result = _weatherForecastService.GetWeatherForecastKeyset(lastId, pageSize, getTotalCount);
return Ok(result);
}
Can be invoked with something like:
http://localhost:5293/WeatherForecast/keyset?lastId=15&getTotalCount=true
Cursor-based
[HttpGet("cursor")]
public IActionResult GetCursorPagination([FromQuery] string? cursor = null, [FromQuery] int pageSize = 15, [FromQuery] bool getTotalCount = false)
{
var result = _weatherForecastService.GetWeatherForecastCursor(cursor, pageSize, getTotalCount);
return Ok(result);
}
Can be invoked with something like:
http://localhost:5293/WeatherForecast/cursor
Which will return an object similar to:
{
"items": [...],
"pageSize": 15,
"nextCursor": "MTU=",
"previousCursor": null,
"hasNextPage": true,
"hasPreviousPage": false,
"totalCount": null
}
A normal flow would require you to get the next page using the nextCursor value like this:
http://localhost:5293/WeatherForecast/cursor?cursor=MTU=
Final Thoughts
API pagination in .NET is not one-size-fits-all.
Pick the right strategy for the scenario, and make pagination a first-class design concern in your API. Doing so will impact scalability, user experience, and long-term maintainability. Offset works when simplicity and page counts matter. Keyset delivers consistent performance at scale. Cursor offers abstraction and stability for external consumers. In practice, most production systems use a mix. The key is to let your API contract dictate the strategy, not database convenience. Build with flexibility in mind today, and you’ll avoid painful migrations tomorrow.
Juan Andrés Leiva
I write about .NET architecture, API design, and DevOps.
👉 Follow for deep technical dives and production-ready strategies.
💬 Share your thoughts or questions in the comments — I reply to all messages.
GitHub · LinkedIn · Medium
.NET API Pagination Explained: Offset vs Keyset vs Cursor Strategies was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Juan Andrés Leiva

Juan Andrés Leiva | Sciencx (2025-08-28T20:42:21+00:00) .NET API Pagination Explained: Offset vs Keyset vs Cursor Strategies. Retrieved from https://www.scien.cx/2025/08/28/net-api-pagination-explained-offset-vs-keyset-vs-cursor-strategies/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.