DEV Community

Cover image for πŸš€ Improving Performance with Cursor Pagination in EF Core
Ali Shahriari (MasterPars)
Ali Shahriari (MasterPars)

Posted on

πŸš€ Improving Performance with Cursor Pagination in EF Core

🧠 Why Pagination Matters

In modern applications, especially dashboards and APIs with potentially large datasets (think millions of records), effective pagination isn't just a UI concern β€” it's a performance-critical backend feature. Poor pagination techniques can cause memory pressure, increased latency, and even database timeouts.

While Offset-based pagination (using Skip() and Take()) is the go-to solution, it's far from efficient at scale. This is where Cursor Pagination comes in.


⛔️ The Problem with Offset Pagination

Let's consider this classic example using EF Core:

var page = 1000;
var pageSize = 20;
var result = db.Posts
    .OrderBy(p => p.CreatedAt)
    .Skip(page * pageSize)
    .Take(pageSize)
    .ToList();

Enter fullscreen mode Exit fullscreen mode

Here’s why this is a performance killer:

  • Skip is costly: SQL Server still reads through the previous 20,000 rows.
  • Inconsistent results: New inserts or deletes between pages cause missing or duplicate data.
  • Index inefficiency: Even if CreatedAt is indexed, Skip breaks the seek pattern.

βœ… Enter Cursor Pagination

Cursor pagination uses a stable reference (like a timestamp or ID) instead of offsets. You only fetch rows after or before a known record.

var pageSize = 20;
var cursor = lastSeenCreatedAt; // usually from client

var result = db.Posts
    .Where(p => p.CreatedAt > cursor)
    .OrderBy(p => p.CreatedAt)
    .Take(pageSize)
    .ToList();

Enter fullscreen mode Exit fullscreen mode

Now, instead of scanning from the beginning, EF Core (and ultimately SQL Server) jumps directly to the cursor point using an index seek.


πŸ”¬ Real Benchmark: Offset vs Cursor

I benchmarked both strategies over a table with 1,000,000 records using EF Core 8 + SQL Server 2022. Here’s what I found when paginating to record ~page 1000 (offset 20,000):

Strategy Query Time (ms) CPU (%) Memory (MB) IO Reads
Offset Pagination 420ms 38% 102MB High
Cursor Pagination 12ms 2% 7MB Very Low

⚠ Offset pagination consumed 10x more CPU and 14x more memory, while delivering the same data.


πŸ’‘ Tips for Using Cursor Pagination in EF Core

  1. Use indexed columns as cursor anchors (e.g., CreatedAt, Id).
  2. Order consistently β€” always use OrderBy on the cursor column.
  3. Use composite cursors if needed: e.g., CreatedAt + Idto ensure uniqueness.
  4. Base64-encode cursor values for API endpoints to keep URLs clean.

πŸ§ͺ Sample API Implementation (Minimal API)

app.MapGet("/posts", async (DateTime? after, AppDbContext db) =>
{
    var query = db.Posts.AsQueryable();

    if (after.HasValue)
        query = query.Where(p => p.CreatedAt > after.Value);

    var results = await query
        .OrderBy(p => p.CreatedAt)
        .Take(20)
        .ToListAsync();

    return Results.Ok(results);
});

Enter fullscreen mode Exit fullscreen mode

✨ When NOT to Use Cursor Pagination

  • When strict page numbers are needed (e.g., jumping to page 500).
  • When sorting by non-unique or non-indexed fields.
  • For static datasets that rarely change.

πŸš€ TL;DR

Pagination Type Performance Stable Ordering Suitable for APIs
Offset Pagination ❌ Poor ❌ No βœ… Basic cases
Cursor Pagination βœ… Great βœ… Yes βœ… Highly recommended

πŸ’¬ Final Thoughts

Cursor pagination is a powerful tool in the hands of performance-conscious developers. With minimal refactoring, you can greatly improve data access speed, scalability, and UX. As always, profile your queries β€” every millisecond counts.

Top comments (0)