DEV Community

Zoe
Zoe

Posted on

Scaling Node.js (Next.js) in Kubernetes: Lessons from a CPU Bottleneck

Intro

When one of my Node.js (Next.js) apps started hitting CPU limits under load, I assumed it'd scale across the two cores I gave it in Kubernetes. It didn't.

Turns out, Node.js is single-threaded - but more importantly, it doesn't automatically scale across cores.

This wasn't news to me. It's well-known that JavaScript is single-threaded after all. But seeing it bite in production? That hit different.

Here's a quick breakdown of how I fixed it using the cluster module, what trade-offs come with it (multi-process ≠ multi-thread), and why matching infra config with your app's process model really matters.

Step 1: Enable Multi-CPU in Node.js

The trick is to fork multiple processes using Node's built-in cluster module - one for each core.

import cluster from 'node:cluster'
import os from 'node:os'

const defaultProcessCount = os.cpus().length
const clusterSize = process.env.PROCESS_COUNT ?? defaultProcessCount

if (cluster.isPrimary) {
  for (let i = 0; i < clusterSize; i++) {
    cluster.fork()
  }

  cluster.on('exit', (worker) => {
    console.log(`worker ${worker.process.pid} died`)
  })
} else {
  // Your app logic here (e.g. HTTP server)
}
Enter fullscreen mode Exit fullscreen mode

Each fork() spawns a brand-new process - they'll all listen on the same port and magically share traffic thanks to Node's internal load balancing.

Step 2: Actually Give It More CPU in Kubernetes

Your app can only use the CPU cores you give it.
In your deployment YAML:

resources:
  limits:
    cpu: "2"
    memory: "2048Mi"
Enter fullscreen mode Exit fullscreen mode

Careful! Forking 4 processes inside a pod with only 1 core = they'll just take turns fighting for it! So always match your infra to your process count.

But This Isn't Multithreading

This setup = multi-process, not multi-threaded.
Each worker is its own process - they don't share memory. That means:

  • More memory usage overall 
  • No shared global state between workers
  • Still works great for I/O-heavy apps (APIs, SSR apps, etc.)

If you're coming from Java or Go, this is different. Those languages handle threads inside the same process and share memory, which is generally more efficient.

Why JS Is Single-Threaded (and That's Okay)

"JavaScript was designed to be single-threaded so that developers could write code without worrying about race conditions and thread safety."
 - MDN Web Docs

JavaScript was born in the browser, where multiple threads would've made DOM access a nightmare.

Instead, async behavior is handled with an event loop - which is why Promise, async/await, and even setTimeout feel intuitive and race-condition-free.

TL;DR

  • Node.js is single-threaded, so you only get one core unless you fork more
  • Use cluster.fork() to spin up one process per core
  • Update your Kubernetes manifest to assign enough CPUs
  • This isn't true multithreading (Java or Go apps will still outperform) but for many use cases, it's good enough!

Thank You!

Thanks for reading! ✨
I also post on my personal blog, where I share notes and experiments.
If you'd like to connect, you can find me on GitHub or LinkedIn. 🚀

Top comments (0)