The Blog

Stop over-fetching your headless CMS

Web performanceHeadless CMS

Most "slow Payload sites" aren't slow because of the framework. They're slow because every query requests the full document tree. We use select and depth to fetch only what each route needs.

The problem

Queries that fetch everything

Payload CMS is relational by default. A blog post can reference an author, categories, a featured image, related posts, and a rich text body with embedded media. By default, Payload populates all of those relationships when you query a document, and each populated relationship can have its own nested relationships.

For a single post page, that's fine. You need the full document to render the page. But a blog listing only needs the title, slug, excerpt, category, and featured image. A sitemap route only needs the slug and updatedAt timestamp. An archive carousel needs even less.

When every route queries the full document tree, the response payload grows, database queries multiply, and TTFB increases. On listing pages that fetch 20 or 50 documents, the overhead compounds. The site feels slow, not because of the frontend or the hosting, but because every query asks for more data than the route actually uses.

The fix

Two settings that change everything

Payload gives you two controls that most projects underuse: depth and select.

depth controls how many levels of relationships Payload populates. The default behaviour populates relationships recursively, a post's author, the author's avatar, the avatar's alt text. Setting depth: 0 returns IDs instead of populated documents. Setting depth: 1 populates one level deep and stops.

For listing pages, depth: 1 is usually enough, you get the post's category name and featured image without also fetching every related post's full document. For sitemaps, depth: 0 is all you need, slugs and timestamps don't require any populated relationships.

select declares exactly which fields to return. Instead of receiving the entire document (title, slug, content, excerpt, meta, image, author, related posts, timestamps, status), you specify: select: { slug: true, updatedAt: true }. The query returns only those fields, and the database query itself is leaner, Payload optimises the underlying fetch based on what you request.

The result

What this looks like in practice

Our sitemap routes query with depth: 0 and select for slug, breadcrumbs (where needed), and updatedAt. The response is minimal, just the data the sitemap XML requires. Database queries are fast. TTFB stays low even as the content library grows.

Blog listing pages use depth: 1 with select for the card fields, title, slug, excerpt, category, and featured image. Enough to render the listing without fetching the full rich text body or related posts from every document.

We also keep overrideAccess: false on public-facing queries so Payload's access control runs normally. Fetching efficiently doesn't mean bypassing permissions, draft content still stays out of public responses.

For pages that need the full document, individual post pages, case studies, we use React.cache wrappers so the same query isn't repeated across components that render on the same page. The full document loads once and is shared.

The result is measurable in TTFB. Listing pages and sitemaps respond faster because they're fetching less data. Individual pages stay fast because repeated queries are deduplicated. The site scales with content volume rather than degrading.

The fastest query is the one that only fetches what it needs. depth and select turn over-fetching into a solved problem.

Web Performance

Is your headless CMS slower than it should be?

We build Payload CMS sites where every query is optimised for its route, listing pages fetch listing data, sitemaps fetch sitemap data, and full documents load only when they're needed.