Self-hosting Next.js: why next start only uses one CPU core

You moved a Next.js app to your own VPS, and under load it gets slow while the server says it is barely busy. That is usually not the database and not the server size. next start runs one Node process, and one process can only fill one core. Here is what that looked like in two real load tests, and what to change.

Two Next.js apps on a 4 vCPU / 8 GB server, 2,500 simulated users, default config

MeasurementUmamiPayload 3
What it isAnalytics app, Next.js and PostgresCMS built into Next.js, with Postgres
Requests sent toThe home page (renders the login screen)The list posts API route
Held about290 requests a second490 requests a second
p95 under 100 users300 ms160 ms
Whole server CPUAbout 40%About 36%
Busiest thread100% of one core100% of one core
DatabasePostgres at 0.05 coresPostgres at about 0.3 cores
Estimate with one process per core610 to 870 requests a secondAbout 770 requests a second

The short answer

next start runs your app as a single Node.js process. Rendering pages, running route handlers and serializing responses all happen on that process's one JavaScript thread. On a 4 core VPS that thread fills one core while the server as a whole reads 25 to 40%, and every request past that point waits in line.

Both apps in the table stopped at exactly that point. Neither ran out of memory, and neither database was anywhere near busy.

What happened in the two runs

Umami: we gave Stresix the GitHub repository and nothing else. It started the app and Postgres from Umami's own docker compose file and sent 2,500 simulated users at the home page. It was healthy up to about 500 users and 290 requests a second. After that, extra requests waited until they timed out, but the app never crashed or returned a 5xx. The umami container used 681 MB and Postgres used 0.05 cores.

Payload: the with-postgres template, built in production mode, with requests to the list posts API. It kept getting faster until about 830 users, held around 490 requests a second and never returned an error. Its Next.js server thread was pinned at 100% from the start, with the server about 36% busy and about 1 GB of memory used.

The estimates in the last row come from the run reports: what one process did, multiplied out to one process per core on the same server. They are estimates, not measurements.

How to tell if your app is hitting this

Run top on the server and press 1 to see each core. One core near 100% with the rest mostly idle, at the moment the app is slow, is this problem. In docker stats, a Next.js container stuck around 100 to 150% on a box with more cores points the same way. The Node.js guide below has more ways to check.

Run one instance per core

On a single VPS the simplest way is several containers from the same image behind a reverse proxy. The Next.js docs already recommend running a proxy such as nginx in front of next start, so this is mostly a matter of starting 3 or 4 copies (for example with replicas in docker compose) and letting the proxy spread requests across them.

Use one build for all of them. Next.js generates a build ID during next build, and the docs say to boot every container from the same build. If you build separately per server, set a fixed build ID with generateBuildId.

Watch memory. Each instance has its own. In these runs one instance used 0.7 to 1 GB, so four of them fit on 8 GB with room for Postgres, while on a 4 GB server two may be the practical limit.

What changes with more than one instance

The cache is per instance. By default Next.js keeps its ISR and data cache in memory (50 MB by default) and on local disk, so each instance has its own copy, and revalidateTag on one instance only clears that one. For a shared cache, the docs describe a custom cacheHandler backed by shared storage such as Redis, with cacheMaxMemorySize set to 0.

Server Actions need one encryption key. Each build generates its own key, and instances with different keys fail with "Failed to find Server Action". Containers started from the same image already share it. If you build more than once, set NEXT_SERVER_ACTIONS_ENCRYPTION_KEY at build time.

Rolling deploys can mix versions. Setting deploymentId in next.config.js lets Next.js notice when a browser is talking to a newer or older build and reload the page instead of breaking.

Anything your own code keeps in memory, such as sessions, rate limit counters or a cache in a module variable, is no longer shared either. Move it to Redis or the database.

Before adding instances, check what renders on every request

Pages that use no dynamic APIs are prerendered at build time and sent with a public Cache-Control header, so a CDN or proxy can cache them. Dynamic pages are sent as private, no-store and rendered on every hit. If a busy route renders the same output for everyone, making it static or revalidated means the one thread does that work once instead of on every request.

Check your own Next.js app

Stresix builds your repository (a Dockerfile, a docker compose file, or Nixpacks when there is neither) on a fresh server, 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

Does next start use all CPU cores?

No. It is one Node.js process, and your rendering and route handlers run on its one JavaScript thread. To use more cores, run several instances behind a proxy.

Will a bigger VPS make my self-hosted Next.js app faster?

Only once every core is busy. In both runs above, the server was about 60% idle while one thread was full. Run more instances first.

How many Next.js instances should I run on one VPS?

One per core is the usual starting point, as long as memory allows. Each instance in these runs used 0.7 to 1 GB, and the database needs room too.

Do multiple Next.js instances need Redis?

Not to run, but by default each instance keeps its own cache. For a shared ISR and data cache, and for revalidateTag to reach every instance, the Next.js docs describe a custom cache handler backed by shared storage such as Redis.

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