Preventing OutOfMemoryErrors with Hibernate Pagination
Learn how to use Hibernate's setFirstResult and setMaxResults to implement database-level pagination and avoid OutOfMemoryErrors in Java applications.
01 Jun 2026, 07:55 UTC

The Danger of the Unbounded Query
A common failure point in Java applications occurs when a developer writes a query to fetch "all active users" or "all transaction logs." While this works in a development environment with ten records, it triggers an OutOfMemoryError in production when the table grows to a million rows. The JVM attempts to materialize every database row into a Java object, exhausting the heap before the first line of business logic even executes.
The solution is to shift the filtering burden from the application memory to the database engine using pagination—the process of requesting only a small, specific subset of data at a time.
Implementing Result Limiting and Offsets
Hibernate provides two primary methods on the Query interface to control the volume of data returned: setMaxResults(int) and setFirstResult(int).
- setMaxResults: This acts as the "page size." It tells the database to stop returning rows once a specific count is reached.
- setFirstResult: This acts as the "offset." It tells the database how many rows to skip before it starts returning results.
Hibernate translates these Java calls into the specific SQL dialect of your database. For example, in PostgreSQL, these become LIMIT and OFFSET clauses; in Oracle, they may translate to FETCH FIRST or ROWNUM filters.
Worked Example: A Paginated User List
Assume you are building a management console that displays 20 users per page. To fetch the third page of results, you need to skip the first 40 users and take the next 20.
// Run this within a Transactional service layer
// Required permissions: Database read access for the 'User' table
public List getUsersPage(int pageNumber, int pageSize) {
int offset = (pageNumber - 1) * pageSize;
return session.createQuery("FROM User u ORDER BY u.username ASC", User.class)
.setFirstResult(offset) // Skip previous pages
.setMaxResults(pageSize) // Limit current page
.getResultList();
}
Verification Steps:
- Set
show_sql=truein your Hibernate configuration. - Execute the method and check the logs. You should see a SQL query containing a
LIMIT 20 OFFSET 40(or equivalent) clause. - Verify that
getResultList().size()is exactly 20.
The "In-Memory Pagination" Trap
There is a critical performance risk when combining pagination with JOIN FETCH. If you attempt to fetch a collection (e.g., JOIN FETCH u.roles) while using setFirstResult, Hibernate may realize it cannot accurately paginate the result set at the SQL level because the join creates duplicate root entities.
In these cases, Hibernate will log a warning: "firstResult/maxResults specified with collection fetch; applying in memory!"
This is a catastrophic failure for performance. Hibernate will pull "every single row" from the database into the JVM and then discard all but the requested page. To avoid this, perform the pagination on a query for IDs first, then fetch the full entities and their collections using those specific IDs.
Limitations of Deep Paging
While setFirstResult is convenient, it suffers from "Deep Paging" degradation. As the offset increases (e.g., OFFSET 100000), the database must still scan through those 100,000 rows to find the starting point, even if it only returns 20. This leads to linear performance decay.
For extremely large datasets, consider Keyset Pagination (or the "seek method"). Instead of an offset, filter by the last ID seen on the previous page: WHERE u.id > :lastSeenId ORDER BY u.id ASC LIMIT 20.
Summary Checklist
- Use
setMaxResultsto prevent heap exhaustion. - Use
setFirstResultfor basic page navigation. - Avoid
JOIN FETCHin the same query as pagination to prevent in-memory processing. - Switch to Keyset Pagination if users frequently access pages deep in the result set.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.