Implementing Content Pagination in Sulu: Offset vs. Cursor Strategies
Decide between offset-based and cursor-based pagination in Sulu CMS to optimize performance for large content collections and high-volume frontend feeds.
29 Dec 2025, 09:00 UTC

The Pagination Performance Gap
When scaling a Sulu CMS project, content collections—such as large product catalogs or extensive news archives—eventually hit a performance ceiling. The core problem is \"deep pagination\": as a user navigates to page 100, the database must scan through all preceding records before returning the requested subset. This results in increased query latency and higher memory overhead on the application server.
The primary engineering decision is whether to stick with the default Offset-based pagination or implement a Cursor-based strategy for high-volume frontend feeds.
Comparing Pagination Strategies
| Feature | Offset-Based (Standard) | Cursor-Based (Custom) |
|---|---|---|
| Implementation | Built-in to Sulu API/Repositories | Requires custom repository logic |
| Performance | Degrades as page index increases | Constant time (O(1) lookup) |
| Consistency | Items shift if content is added/deleted | Stable pointers to specific records |
| UX Support | Supports specific page numbers (1, 2, 3) | Supports \"Load More\" or Infinite Scroll |
Engineering Trade-offs
Offset-based pagination is the default behavior for Sulu administrative views and standard API endpoints. It is ideal for internal management tools where the total dataset is manageable and administrators need to jump to a specific page. However, it relies on the OFFSET clause in SQL, which forces the database to read and discard rows, leading to linear performance degradation.
Cursor-based pagination (also known as keyset pagination) avoids the offset entirely. Instead of a page number, the client sends a \"cursor\"—usually the ID or timestamp of the last item seen. The query then filters for records WHERE id > :last_id. This ensures that the database uses an index to jump directly to the next set of results, regardless of how deep the user has scrolled.
Implementing a Custom Paginated Provider
To implement a performant feed in Sulu, you should extend the repository layer to handle custom limit and cursor parameters. This example assumes a Sulu 2.x environment leveraging Symfony components.
Implementation Steps:
- Create a custom repository method that accepts a
lastIdinstead of apagenumber. - Ensure the query is ordered by a unique, indexed column (typically the primary key).
- Return a response containing both the data and the cursor for the next page.
// In your custom Content Repository public function findPaginatedFeed(int $limit, ?int $lastId = null): array { $qb = $this->createQueryBuilder('content') ->orderBy('content.id', 'ASC') ->setMaxResults($limit); if ($lastId !== null) { // Use the cursor to jump directly to the next record $qb->andWhere('content.id > :lastId') ->setParameter('lastId', $lastId); } return $qb->getQuery()->getResult(); } Validation and Diagnostics
To verify the efficiency of your pagination choice, you must profile the database execution plan. Run the following checks on your development environment:
- Query Execution: Use
EXPLAIN ANALYZEon the generated SQL. An offset-based query will show a high number of \"rows removed by filter\" as the page number increases. A cursor-based query should show an \"Index Scan\" with a constant number of rows processed. - API Response: Validate that your JSON response includes the
next_cursorvalue. If the result set is smaller than thelimit, thenext_cursorshould be null, signaling the end of the collection. - Memory Profiling: Use the Symfony Profiler to compare the memory peak between requesting page 1 and page 50. If memory usage spikes significantly on page 50, your implementation is likely still performing a full scan.
Limitations
Cursor-based pagination prevents the use of \"Jump to Page X\" navigation. If your business requirements mandate a numbered pagination bar, you must use offset-based pagination and mitigate performance hits through aggressive caching of the total count and result sets using the Sulu cache layer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.