Why is my Node.js app only using one CPU core?
Your app slows down under load, but the server says it is only 40% busy. The database looks idle too. Before you buy a bigger server or start rewriting queries, check one thing: whether a single Node thread is at 100% of one core. In all six Node apps in the table below, that was the limit.
Real runs on the same 4 vCPU / 8 GB server, 2,500 simulated users
| App | Runtime | Requests a second | Whole server CPU | What ran out |
|---|---|---|---|---|
| Stresix (this site) | Node.js | About 2,600 at peak | About 36% | One thread at 100% |
| Payload 3 | Node.js (Next.js) | About 490 | About 36% | One thread at 100% |
| Umami | Node.js (Next.js) | About 290 | About 40% | One thread at 100% |
| Strapi 5 | Node.js | About 290 | About 39% | One thread at 100% |
| Directus 11 | Node.js | About 220 | About 35 to 40% | One thread at 100% |
| Ghost 6 | Node.js | About 115 | About 37% | One thread at 100% |
| Gitea | Go | About 920 | Over 90% | Every core |
| WordPress 6.8 | PHP (Apache) | About 76 | 100% | Every core |
The short answer
Node.js runs your JavaScript on one thread per process. However many cores the server has, one node process can only keep one of them busy with your code. On a 4 core server that looks like 25 to 40% CPU in total while the app is completely full and requests are queuing.
Node does use a few helper threads for garbage collection and some I/O, which is why a node process can show a bit more than 100% in top. The thread that runs your request handlers still cannot go past one core.
What it looks like under load
The table above is from real runs, all on the same server size with the same ramp to 2,500 simulated users. The apps do very different work per request, so compare the last two columns, not the requests a second.
Every Node app had the same shape. Requests a second climbed, then stopped growing. Past that point extra users either waited in line, so p95 kept rising, or got turned away (Directus answered 503, Ghost 429). The server as a whole stayed between 35 and 40% busy, and where there was a database, it never used more than half a core. The busiest thread inside the app container was at 100%.
Gitea (written in Go) and WordPress (PHP under Apache) are the contrast. Both spread the work across all four cores and ran out of CPU on every one of them. That is the case where a bigger server actually helps.
How to check your own server
Do not trust a total CPU graph. Most hosting dashboards average all cores, so one full core on a 4 core box shows up as 25%.
On the server, run top and press 1. That shows each core on its own line. If one core is near 100% while the others are mostly idle, and the app is slow at the same moment, this is your problem.
To be sure it is your app, run top -H -p followed by the process id of node. That lists the threads inside the process. One thread near 100% confirms it.
In docker stats, 100% means one full core. A Node container stuck around 100 to 150% on a box with more cores is a strong hint.
Why Node works this way
Node is built around one event loop. While a request waits on the database or another API, the thread moves on to the next one, so a single thread can juggle thousands of slow, waiting requests. That is why it works so well for I/O.
The catch is the work that is not waiting: serializing JSON, rendering React on the server, validating input, turning database rows into objects. All of that runs on the one thread, one request at a time. Once that adds up to a full core, the process cannot go any faster, however idle the rest of the server is.
The fix: one process per core
Run several copies of the app on the same server and spread the requests between them. There are three common ways.
Several containers behind a reverse proxy. Run the app 3 or 4 times (for example with replicas in docker compose) and put nginx, Caddy or Traefik in front. This works for any framework, including Next.js, and is easy to reason about.
PM2 in cluster mode. pm2 start server.js -i max starts one process per core and shares the port between them.
The node:cluster module. A few lines at the top of your server file fork one worker per core. This is what PM2 does for you.
Start with one process per core. If the database runs on the same server, leave it some room. worker_threads is a different tool: it is for moving one heavy job (resizing an image, hashing) off the main thread, not for spreading normal requests across cores.
What to change before running several processes
Each process has its own memory, so anything kept in memory is no longer shared. Sessions, caches, rate limit counters and WebSocket rooms need to move to a shared store such as Redis or the database, or users will see different results depending on which process answers.
Scheduled jobs run once per process. If the app starts a cron job on boot, four processes run it four times. Run it in one place only.
Memory and database connections multiply. Four processes use roughly four times the memory, and each opens its own connection pool. With a pool of 20 and four processes you need 80 connections, and Postgres allows 100 by default.
When it is not one core
If every core is busy, like Gitea and WordPress above, the CPU really is the limit. A bigger server or less work per request will help, and a profiler will show where the time goes.
If nothing on the server is busy and latency still climbs, the limit is outside the app: usually the database, its connection pool, or an external API you call.
Check your own app
Stresix measures CPU per core and per thread inside each container while it ramps up the load, and the report says it plainly when one thread was the limit and whether a bigger server would help. Point it at a GitHub repository or Docker image. Your first run is free (Lite: up to 500 simulated users in a 4 minute window on 2 vCPU / 4 GB, where one full core shows up as about 50%).
Frequently asked questions
Does Node.js use multiple CPU cores?
Your JavaScript runs on one thread per process. Node uses a few helper threads for garbage collection and some I/O, but to use every core for requests you run several processes, one per core.
Will a bigger server make my Node.js app faster?
Not if one thread is the limit. In the runs above, each Node app left about 60% of a 4 vCPU server idle. Run more processes first, and only pay for more cores once every core is busy.
How many Node.js processes should I run?
Start with one per core. Leave room for the database if it shares the server, and check memory and database connections, since each process adds its own.
Is PM2 cluster mode the same as running several containers?
For CPU, yes: both run several processes and share the traffic between them. PM2 does it inside one container or host. Several containers behind a proxy does it at the container level, which is often simpler with docker compose.
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