Paginated REST Endpoints with Quarkus Panache and Reactive Hibernate ORM
Use Quarkus Panache with reactive Hibernate ORM to paginate REST results without blocking worker threads, plus a worked example and limits to watch.
12 Nov 2026, 08:51 UTC

The problem: blocking the worker thread for every list request
You are building a Quarkus REST API and need to expose a collection of blog items. A straightforward implementation calls repository.findAll(), maps the result to JSON, and returns it. While the dataset is small, this works fine. As the table grows, every request loads all rows into memory and ties up a worker thread until the database finishes. Under concurrent load, threads exhaust and response times climb even though the CPU is mostly idle.
The useful takeaway: Quarkus Panache with reactive Hibernate ORM translates pagination into OFFSET and LIMIT automatically, returns results through Mutiny Uni or Multi streams, and releases the thread while waiting for the database.
How Panache reactive pagination works
Panache is Quarkus’s active-record wrapper around Hibernate ORM. In reactive mode, repository methods return Mutiny types instead of blocking collections. A Uni represents a single asynchronous result; a Multi represents a stream. The Page object carries the requested page number, page size, and total count so the client can render navigation controls.
When you call page(Page.of(page, size)) on a PanacheQuery, Panache adds OFFSET and LIMIT to the generated SQL. Because the query runs on the reactive database client, the server can handle other requests while the database cursor is open.
Worked example: paginated items endpoint
This example assumes Quarkus 3.x and a PostgreSQL database. Adjust the datasource extension for your database.
Add extensions
Run this in the project root as a normal user:
# Verify coordinates for your Quarkus 3.x release before running
mvn quarkus:add-extension \
-Dextensions="quarkus-rest-reactive,quarkus-hibernate-orm-panache,quarkus-jdbc-postgresql"
- Where to run: terminal in the Maven or Gradle project root.
- Permissions: normal user; no sudo required.
- Placeholders: replace
quarkus-jdbc-postgresqlwith the reactive datasource extension matching your database if you are not using PostgreSQL. - Expected check:
BUILD SUCCESSand the extensions appear inpom.xml. - Risk: incorrect extension coordinates or network issues can fail dependency resolution.
Entity and repository
package com.example.blog;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class Item extends PanacheEntity {
public String title;
public String body;
}
package com.example.blog;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class ItemRepository implements PanacheRepository {
}
REST resource
package com.example.blog;
import io.quarkus.hibernate.orm.panache.PanacheQuery;
import io.quarkus.panache.common.Page;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
@Path("/items")
public class ItemResource {
@Inject
ItemRepository repository;
@GET
public Uni> items(
@QueryParam("page") Integer page,
@QueryParam("size") Integer size) {
int requestedPage = page != null ? page : 0;
int requestedSize = size != null ? size : 20;
PanacheQuery query = repository.findAll();
return query.page(Page.of(requestedPage, requestedSize));
}
Confirm the exact return type against your Quarkus 3.x patch release; some versions return Uni> or a similar page wrapper. The compiler will tell you if the signature needs adjustment.
Database configuration
quarkus.datasource.db-kind=postgresql
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/blog
quarkus.datasource.username=bloguser
quarkus.datasource.password=changeme
quarkus.hibernate-orm.database.generation=drop-and-create
Use drop-and-create only in development. For production, use migrations and a managed connection pool.
Call the endpoint
curl -i "http://localhost:8080/items?page=0&size=10"
A successful response should include the requested page number, page size, total item count, and an items array. Exact JSON field names depend on the Quarkus and Jackson version, so inspect the actual payload rather than assuming it.
Trade-offs and limits
- JPA only: Panache pagination relies on JPQL or Criteria queries. Database-specific functions or complex native SQL must be written manually, which reduces portability.
- Memory: Large page sizes load many entities into the JVM. Prefer modest sizes, such as 50 or fewer, and consider cursor-based pagination for deep pages.
- Quarkus version: Full reactive Panache support requires Quarkus 3.x. Older releases lack the reactive query capabilities used here.
- Driver: Use a reactive database client for your database; blocking JDBC in a reactive endpoint negates the thread-release benefit.
Verify the result
- Start the application in dev mode:
mvn quarkus:dev. - Call
curl "http://localhost:8080/items?page=0&size=10". - Check that the HTTP status is 200 and the body contains pagination metadata plus an
itemsarray. - Change
page=1and confirm the returned items differ from page 0.
If the response lacks metadata, confirm that the returned object is the Panache page wrapper and not a plain List. No rollback is required for this example because it only adds source files and dependencies; remove them if you need to revert.
Pagination with Panache reactive Hibernate ORM removes boilerplate and protects worker threads, but it is not a substitute for careful page sizing or for queries that fall outside JPA. Start with small pages, verify the JSON shape, and add cursor-based pagination when users need to page deep into large result sets.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.