How Payload CMS Scales for High-Traffic Applications in Production

Published on by

Execudea

Payload CMS scales well for high-traffic applications because the expensive part, rendering pages for visitors, does not have to touch the CMS at all. With static generation or ISR in Next.js, a CDN in front, on-demand revalidation from Payload hooks, indexed database fields, lean queries and stateless containers, a single Payload deployment can serve very large traffic volumes. The CMS then only handles editor activity and cache misses.

This guide covers the architecture and the specific settings we use when running Payload CMS in production.

Understand Where the Load Actually Goes

In a typical Payload 3 project, one Next.js application contains three things: the admin panel, the API (REST, GraphQL and the Local API), and your public frontend. Scaling becomes simple once you separate the two kinds of traffic:

  • Visitor traffic is read-heavy and highly cacheable. This should be served from static HTML and a CDN wherever possible.
  • Editor and API write traffic is small in volume but must always hit the database. This is what your Payload server and database are actually sized for.

If every page view triggers database queries, you will hit limits quickly no matter how big the server is. If page views are served from cache, even a modest server goes a long way.

1. Serve Pages Statically and Revalidate on Change

Generate public pages at build time or on first request, then cache them. Instead of short time-based revalidation, trigger revalidation exactly when content changes using a Payload afterChange hook:

import type { CollectionAfterChangeHook } from 'payload'
import { revalidatePath, revalidateTag } from 'next/cache'

export const revalidatePost: CollectionAfterChangeHook = ({ doc, previousDoc, req }) => {
  if (req.context.disableRevalidate) return doc

  if (doc._status === 'published') {
    revalidatePath(`/blog/${doc.slug}`)
    revalidateTag('posts-list')
  }

  // Page was unpublished or its slug changed: clear the old path too
  if (previousDoc?._status === 'published' && previousDoc.slug !== doc.slug) {
    revalidatePath(`/blog/${previousDoc.slug}`)
  }

  return doc
}

Attach it in your collection config with hooks: { afterChange: [revalidatePost] }, and add a matching afterDelete hook. Pages stay instantly fresh for editors while visitors almost always get cached HTML. The disableRevalidate context flag lets you skip this during bulk imports.

2. Put a CDN in Front of Everything

Cache static pages, Next.js assets and media at the edge with Cloudflare, CloudFront or your host's CDN. Exclude /admin and /api from caching so editors and authenticated requests are never served stale or shared responses.

3. Index the Fields You Query

Every field you filter or sort on in production queries should be indexed. Slugs, status fields, dates and relationship fields used in where clauses are the usual candidates.

fields: [
  { name: 'slug', type: 'text', required: true, unique: true, index: true },
  { name: 'publishedAt', type: 'date', index: true },
  { name: 'category', type: 'relationship', relationTo: 'categories', index: true },
]

For multi-field queries, Payload also supports compound indexes at the collection level through the indexes property. On PostgreSQL, check slow queries with EXPLAIN ANALYZE before adding indexes blindly.

4. Keep Queries Lean: depth, select and limit

The most common Payload performance problem is over-fetching. By default, relationships are populated to a depth of 2, which can turn one query into a large tree of joined data.

const posts = await payload.find({
  collection: 'posts',
  where: { _status: { equals: 'published' } },
  sort: '-publishedAt',
  limit: 12,
  depth: 0,
  select: { title: true, slug: true, publishedAt: true, heroImage: true },
})
  • depth: 0 returns relationship IDs only. Increase it only where you actually render related data.
  • select returns only the fields you need, which is especially important when documents contain large rich text or block fields.
  • limit and pagination should always be explicit on listing pages. Never fetch an entire collection to render a page.
  • Use the Local API in server components and route handlers. It skips HTTP overhead entirely.

5. Configure Database Connection Pooling

With PostgreSQL, every running container opens its own connection pool. Multiply containers by pool size and you can exhaust the database's connection limit long before CPU becomes a problem.

import { postgresAdapter } from '@payloadcms/db-postgres'

db: postgresAdapter({
  pool: {
    connectionString: process.env.DATABASE_URI,
    max: 10,
  },
}),

Keep max modest per instance and use PgBouncer (or your managed provider's pooler) once you run several containers or serverless functions. With MongoDB, managed clusters such as Atlas handle this well, but the same principle of predictable pool sizes applies.

6. Move Media Off the Application Server

Storing uploads on the local disk ties media to one container and makes horizontal scaling impossible. Use an official storage adapter so uploads go straight to object storage:

import { s3Storage } from '@payloadcms/storage-s3'

plugins: [
  s3Storage({
    collections: { media: true },
    bucket: process.env.S3_BUCKET!,
    config: {
      region: process.env.S3_REGION,
      endpoint: process.env.S3_ENDPOINT, // e.g. DigitalOcean Spaces
      credentials: {
        accessKeyId: process.env.S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
      },
    },
  }),
],

Serve that bucket through a CDN, and use Payload's imageSizes on the media collection to generate resized variants once, at upload time, instead of resizing on every request.

7. Run Stateless Containers and Scale Horizontally

Once media is in object storage and sessions use JWTs, Payload instances hold no local state. That means you can run multiple containers behind a load balancer and add more as traffic grows. A practical setup:

  • Build Next.js with output: 'standalone' for small Docker images.
  • Run two or more containers behind Nginx, a cloud load balancer or your platform's router.
  • Use a managed PostgreSQL or MongoDB service with automated backups.
  • Keep secrets in environment variables, never in the image.

8. Offload Heavy Work to Background Jobs

Sending emails, syncing with third-party APIs, generating reports and bulk imports should not run inside a request. Payload's built-in Jobs Queue lets you define tasks and run them outside the request cycle, so editor saves stay fast even when hooks trigger expensive work.

9. Monitor Before You Need To

  • Track response times and error rates for /api routes separately from public pages.
  • Enable slow query logging on your database.
  • Watch Core Web Vitals in Search Console. Speed problems usually appear there first.
  • Load-test the uncached paths (search, filters, authenticated pages), because those are the ones that hit Payload directly.

Production Scalability Checklist

AreaSetting
RenderingStatic generation / ISR with hook-based revalidation
CachingCDN for pages, assets and media; bypass for /admin and /api
DatabaseIndexes on queried fields, connection pooling, managed service
Queriesdepth: 0 by default, select, explicit limits
MediaS3-compatible storage adapter + CDN, pre-generated image sizes
ComputeStateless containers, horizontal scaling behind a load balancer
Background workJobs Queue instead of in-request processing

Running multiple sites from one instance? The same principles apply to multi-tenant Payload CMS deployments, with tenant-aware cache tags added on top.

Get a Production Review

Execudea designs, deploys and tunes Payload CMS applications on DigitalOcean, AWS and Docker-based infrastructure. If your Payload site is slowing down under traffic, or you are planning a high-traffic launch, talk to our Payload CMS development team about a performance and architecture review.

Frequently Asked Questions

Can Payload CMS handle high-traffic websites?

Yes. When public pages are statically generated and cached on a CDN, visitor traffic rarely reaches the CMS. Payload then only handles editor activity and cache misses, which a modest, horizontally scalable deployment handles comfortably.

Is PostgreSQL or MongoDB better for scaling Payload CMS?

Both scale well. PostgreSQL suits strongly relational data and teams that want SQL tooling; MongoDB suits document-heavy, flexible schemas. Query design, indexing and caching matter far more than the choice of adapter.

Can I run multiple Payload CMS instances behind a load balancer?

Yes, as long as media is stored in object storage (S3, DigitalOcean Spaces, etc.) and not on local disk. Payload instances are then stateless and can run behind a load balancer.

What is the most common cause of slow Payload CMS queries?

Over-fetching. Relationship depth defaults to 2, which can pull large amounts of related data. Use depth: 0, select and explicit limits on every production query.