Payload CMS performance under load

Payload 3 was the fastest of the five CMSes in our load test, and it never returned an error. It also left about 60% of the server idle, because Payload runs inside one Next.js process and that process can only fill one CPU core. Here is what one instance did, and what to change when you need more.

Payload 3 + Postgres 17, one instance, default config

Simulated usersWhat Payload did
Under 100324 requests a second, p95 160 ms
About 250492 requests a second, p95 640 ms
About 500495 requests a second, p95 1.6 s
About 1,000465 requests a second, p95 3.4 s
About 1,500456 requests a second, p95 3.4 s
2,400 to 2,500424 requests a second, p95 6.8 s
Whole run217,142 requests answered, 0 errors

The short answer

Payload 3 installs into a Next.js app, so in production it runs as one Node.js process, and Node runs your JavaScript on one thread. In our run that thread was at 100% of a core from the start. Payload kept getting faster until about 830 simulated users, held around 490 requests a second, then slid to about 425 as the queue grew. The server as a whole stayed about 36% busy.

So one Payload instance on a 4 core server uses one core for requests. To serve more, run more instances before you pay for a bigger server.

What we saw in a load test

We used the Payload with-postgres template, built in production mode, with Postgres 17 in one docker compose stack on a fresh 4 vCPU / 8 GB server (shared vCPU). It got 10 posts, and 2,500 simulated users ramped up over 10 minutes against the list posts REST API. The table above is what came back.

What ran out was the Next.js server thread. Postgres used about 0.3 cores and Payload about 1 GB of memory, so neither the database nor memory was close to the limit. Payload answered about 1.6 times as many requests as Strapi on the same server, and Strapi was capped the same way.

Ask for less on every request

Payload populates relationships 2 levels deep by default (defaultDepth is 2). On a list endpoint, that can mean pulling and serializing every related document for every item. If a page only needs IDs or a few fields, pass depth=0 or depth=1 and use the select parameter to return only the fields it uses. Less data to turn into JSON is less work for the one thread that is full.

Our test used the default depth on posts with no relationships, so it does not show how much this saves. On collections with relationships and uploads it is usually the first thing worth measuring.

Run more than one instance

Run several containers from the same image behind a reverse proxy such as nginx, Caddy or Traefik, for example with replicas in docker compose. The Payload Docker guide already builds with the Next.js standalone output, so the same image runs as many times as you start it.

From what one instance did here, 4 instances on this server were estimated at about 770 requests a second. That is an estimate, not a measurement, so measure again after the change. Because Payload runs on Next.js, the Next.js side of running several instances applies too (one build for all instances, per instance caches, the Server Actions key). The self-hosting Next.js guide linked below covers those.

What to sort out before running several instances

Same PAYLOAD_SECRET everywhere. It signs logins, so an instance with a different secret rejects tokens the others issued. Start every instance with the same environment.

Uploads. By default Payload stores uploads on the local disk of the instance that received them. With several containers, use a storage adapter (S3, Azure Blob Storage, Google Cloud Storage, Uploadthing or Vercel Blob are supported) or mount one shared volume, or files will exist on one instance only.

Jobs. If you use jobs.autoRun, it runs inside the Next.js process, so each instance runs it on its own schedule. Decide whether you want that, or turn it on for one instance only.

Database connections. The Postgres adapter passes its pool option to node-postgres, which keeps up to 10 connections per pool by default. Four instances can open 40, and Postgres allows 100 by default.

Other ways to take load off it

Cache public reads. If the same published content goes to everyone, a CDN or reverse proxy cache with a short TTL turns a crowd of identical requests into one request to Payload.

Buy a bigger server last. A single instance will not use the extra cores. More CPU only helps once you run more instances and every core is busy.

Check your own Payload app

Your collections, relationships and depth settings change the number, so the run above is a reference, not your answer. Stresix builds your Payload repository (a Dockerfile, a docker compose file, or Nixpacks when there is neither) on a fresh server with its database, ramps up simulated users, and measures CPU per core and per thread inside each container. The report says when one thread was the limit and whether more instances or a bigger server would help. Your first run is free.

Frequently asked questions

Is Payload CMS fast?

In our test it was the fastest of five self-hosted CMSes: about 490 requests a second from one instance on a 4 vCPU / 8 GB server, with zero errors. It was still limited by one Next.js thread, with most of the server idle.

Does Payload use multiple CPU cores?

One Payload instance is one Next.js process, so your requests run on one thread and one core. Run several instances behind a reverse proxy to use the others.

Do I need S3 to run several Payload instances?

You need uploads that every instance can see. A storage adapter such as S3 is the usual way, and a shared volume works on a single server.

Why is my Payload REST API slow?

Check depth first. Payload populates relationships 2 levels deep by default, which can make list endpoints much heavier than they need to be. Then check whether one Node thread is at 100% while the server is mostly idle.

Related guides

Stop guessing your specs. Start knowing them.

Point Stresix at a repo or Docker image and get a number you can deploy on, in one run. Your first run is free, no card.

Start a free load test