When getResources Starts to Hurt: Moving MODX Pagination to pdoPage
getResources struggles on deep MODX resource trees. Here's how to migrate paginated listings to pdoPage, with a worked example, verification steps, and the real trade-offs.
05 Apr 2026, 23:43 UTC

Your MODX blog index worked fine with 30 posts. At 300, the page takes three seconds and the database log shows a query scanning half the resource tree. This is the classic getResources ceiling, and the fix is usually not more caching — it is switching the listing snippet to pdoTools' pdoPage and letting it generate the pagination SQL properly.
The thesis is simple: getResources is a general-purpose listing tool that was never designed for deep, paginated trees, while pdoPage exists specifically to render paginated collections efficiently. On MODX Revolution 2.8 and later, pdoTools ships with the core, so the migration costs you an afternoon, not a dependency.
Why getResources slows down on large trees
getResources builds its result set by walking resources and applying filters in a way that often forces broad scans before limiting. On a shallow tree you never notice. On a site with hundreds of resources under a blog container — especially with TVs (template variables) joined in for sorting or display — the query count and row volume grow faster than the page count.
The second trap is the N+1 pattern: templates that pull related fields per row, so a 20-item listing quietly executes 20 extra queries. getResources makes this easy to do accidentally because TVs and related objects are fetched per resource unless you plan around it.
What pdoPage does differently
pdoPage is the pagination-focused snippet in the pdoTools package. It builds a single, targeted SQL query with LIMIT and OFFSET (or a seek-style approach on MySQL for very deep paging), and it handles the page-number navigation itself. Because it is built on xPDO — MODX's ORM layer — you can express the same parents/depth/TV filters you used in getResources, but the generated SQL is leaner and easier to inspect.
Two properties matter most for the performance/freshness trade-off:
pageLimit— items per page. Keep it aligned with what the design actually shows; fetching 50 to display 10 wastes the optimization.cacheableandcachePeriod— for mostly-static listings, caching the rendered output avoids repeated database hits entirely. For frequently changing content, leave the snippet uncached and rely on the efficient query.
A worked migration example
A typical getResources call for a paginated news listing might look like this:
[[!getPage?
&element=`getResources`
&parents=`12`
&depth=`2`
&limit=`10`
&includeTVs=`1`
&includeContent=`1`
&tpl=`newsRow`
]]The pdoPage equivalent, run in the same template or page where the old call lived (no special permissions needed beyond editing the resource in the Manager):
[[!pdoPage?
&element=`pdoResources`
&parents=`12`
&depth=`2`
&limit=`10`
&includeTVs=`newsImage,newsDate`
&includeContent=`1`
&tpl=`newsRow`
&pageLimit=`10`
&cacheable=`0`
]]
[[!+page.nav]]Two things to notice. First, &includeTVs now names the specific TVs instead of 1 — fetching only what the template uses is one of the real wins, because it avoids joining and hydrating TV data you never render. Second, the pagination controls come from the [[!+page.nav]] placeholder, which pdoPage populates; you no longer need a separate getPage wrapper.
Your newsRow chunk usually works unchanged, since both snippets expose resource fields and TVs as placeholders with the same names. If you merged chunks between the two approaches earlier, watch for placeholder collisions — prefixing chunk names per snippet (e.g., pdNewsRow) avoids confusing output.
Verify the improvement, don't assume it
Before and after the switch, run the listing with the log enabled:
[[!pdoPage?
...
&showLog=`1`
]]With MODX in development mode, this prints the generated SQL and timing at the bottom of the page. Check that the SELECT includes a LIMIT clause and that the WHERE conditions match your intended parent scope — a missing or wrong parents value is the most common cause of empty results or listings leaking across unrelated trees.
For a realistic comparison, build a test tree of 200+ resources, load the page with both snippets, and record page load time and query count. Then clear the MODX cache (Manager > Manage > Clear Cache, or php index.php cache/clear from the site root on the command line) and re-run, so you know the gain comes from the query and not from a warm cache.
The trade-offs
pdoPage is not a drop-in for every getResources habit. Sorting by TV values still costs joins, and very complex filtering (nested conditions, custom where clauses) requires learning pdoTools' &where JSON syntax, which is stricter than getResources' looser parameters. Also, if you are on MODX Evolution rather than Revolution, none of this applies directly — Evolution users need community forks or hand-written SQL to get equivalent pagination performance.
There is also a correctness risk in partial migrations: if some pages use getPage/getResources and others use pdoPage with shared chunks, placeholder collisions can produce subtly wrong output. Migrate one section at a time and check rendered HTML, not just the logs.
Closing
If your listing pages are slow and the resource tree is deep, try pdoPage on your worst-performing page first: name your TVs explicitly, set pageLimit to match the design, verify with &showLog=1, and measure against a cleared cache. Fifteen minutes of comparison will tell you whether to roll it out site-wide — and you will have the query log to prove it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.