5.2Performance, clustering & scaling
The essentials in 30 seconds
- Single-threaded: one Node process uses only one core — clustering (cluster module or PM2) launches one instance per core.
- Cache first: cache expensive results (memory or Redis) — often the highest-impact lever.
- Measure before optimising: profile to find the real bottlenecks — never optimise prematurely.
Running case — Salim absorbs the load in Algiers
Salim's typed API in Algiers starts slowing down: the shop's home page takes time to respond at peak hours. Instead of rewriting everything, he measures: catalogue queries keep coming back and the database is hit for nothing. He enables clustering to use the server's cores and caches the catalogue. Continuation of case 5.1 — Salim optimises what he measured, not at random.
Continuation of case 5.1 — Salim applies the golden rule: measure first, optimise after.
1. Clustering and worker threads
Node is single-threaded: one process uses only one core, while servers have several. Clustering (the native cluster module or PM2) launches several instances of the application (one per core) that share incoming requests — using all cores. PM2 adds automatic restart, monitoring and logs. Worker threads, in turn, run CPU-intensive tasks (heavy computations that would block the event loop) on separate threads. Salim launches his API in cluster mode: capacity is multiplied without changing a line of logic.
Reflex: if a single process runs on a multi-core machine, resources are sleeping — clustering is Node's standard vertical scaling.
2. The levers that pay off
Other levers: caching (memory or Redis — storing expensive results so they are not recomputed, often the highest-impact), database query optimisation (indexing, avoiding the N+1 problem — the database is very often the real bottleneck), not blocking the event loop (no heavy synchronous operations on the main thread), parallelism (Promise.all for independent operations), and horizontal scaling (several instances behind a load balancer — hence the value of stateless applications and JWTs). Salim caches the catalogue: repeated requests no longer hit the database.
Typical order of return: database (indexes, N+1), cache, clustering, then horizontal scaling — always guided by measurement.
3. The right approach: measure, target, verify
Essential principle: do not optimise prematurely. Write correct, clear code first, measure (profiling, metrics, request times) to identify the real bottlenecks when they occur, optimise in a targeted way where it counts, then re-measure to confirm. Optimising blind wastes time, complicates the code and almost always misses the real problem — intuition misleads, measurement decides. Salim profiles before touching the code: the bottleneck was the database, not his loops.
Node excels at I/O (servers, APIs, real time) — the single-thread limit concerns the CPU, not I/O, and most web applications are I/O-bound. Used well, Node serves very large loads.
🔎 Concrete example — Salim uses the cores
Clustering with PM2, then caching.
# launch one instance per core
pm2 start app.js -i max
# measure BEFORE optimising (profiling)
# cache an expensive result (Redis)
# check: nothing blocks the event loop
Reading: each line is a separately testable step.
Lesson: the database is often the real bottleneck — measure it first.
Section vocabulary
- Clustering
- Several instances of the app (one per core) sharing incoming requests.
Simple example: Salim launches his API with PM2 in cluster mode.
What it is not: a Node that would use all cores automatically.
Reflex: how many cores are sleeping on my server?
- Worker threads
- Separate threads for heavy computations, without blocking the loop.
Simple example: Salim delegates a bulky export that froze the API to them.
What it is not: clustering itself.
Reflex: is this computation blocking the main thread?
- Caching
- Storing expensive results so they are not recomputed.
Simple example: Salim's catalogue served from the cache, no longer from the database.
What it is not: adding servers before caching.
Reflex: which queries keep coming back identically?
- Profiling
- Measuring to identify the real bottlenecks before optimising.
Simple example: Salim discovers the slowness comes from an unindexed query.
What it is not: optimising his loops by intuition.
Reflex: did I measure before touching the code?
- Horizontal scaling
- Several servers behind a load balancer.
Simple example: Salim plans replicable instances thanks to his stateless app.
What it is not: using the cores of a single machine.
Reflex: does my app replicate easily (stateless)?
In practice — Optimise and scale an application
- Use clustering (cluster module or PM2) to launch one instance per core and use the whole processor (Node being single-threaded).
- Cache an expensive result (a frequent query) — in memory or with Redis — and measure the gain.
- Profile to identify the real bottlenecks BEFORE optimising (measure before optimising); check that nothing blocks the event loop.
- Understand horizontal scaling: several instances behind a load balancer, and why a “stateless” application (JWT) replicates easily.
Ready-to-say script — justifying an optimisation
“In Algiers, our API slows down at peak hours. I measured: catalogue queries are saturating the database. I suggest two targeted moves: clustering to use all cores, and caching the catalogue. We re-measure afterwards, and we only add servers if the measurement justifies it.”
To say to your manager to get a measured optimisation approved instead of a blind rewrite.
Copy-paste template — perf diagnostic card
PERF — DIAGNOSTIC
SYMPTOM : _______________ (when? ___)
MEASURE : response time ___ · slowest request ___
SUSPECTED BOTTLENECK : database / missing cache / blocked loop / CPU: ___
TARGETED ACTION : clustering / cache / index / worker : ___
RE-MEASURE : before ___ → after ___ · GAIN : ___
Solo / small budget
Salim alone: PM2 in cluster mode + in-memory cache is enough to absorb peaks with no new server.
Small business / small team
Index slow queries first: often the fastest gain, with no infrastructure.
SME in Algiers
Salim schedules measurements at peak hours in Algiers, with a re-measurement protocol after each change.
Team starting out
Pair: one profiles while the other prepares the cache; joint review of measurements before any optimisation.
✓ Best practices
- Use all cores with clustering (PM2 in production).
- Cache expensive results; optimise database queries (indexes, N+1).
- Measure (profile) before optimising, and target the real bottlenecks.
✗ Common pitfalls
- Leaving a single process using only one core.
- Optimising prematurely (wasted time, needless complexity).
- Blocking the event loop with heavy CPU computation on the main thread.
Key takeaways
- Node being SINGLE-THREADED: CLUSTERING (cluster / PM2) launches one instance per core to use ALL cores. WORKER THREADS for heavy CPU.
- Levers: CACHING (Redis — often the highest-impact), QUERY optimisation (indexing, avoiding N+1), not BLOCKING the loop, parallelism (Promise.all).
- MEASURE (profile) before optimising (golden rule); identify the real bottlenecks instead of optimising blind.
- HORIZONTAL SCALING: several servers behind a LOAD BALANCER (favours stateless/JWT apps). Do NOT optimise prematurely.
48h challenge — apply tomorrow
Fill in the diagnostic card for 1 real slowness in your app: symptom, measurement, suspected bottleneck — without touching the code yet.
- I can explain clustering vs worker threads in one sentence.
- My diagnostic card (measurement before optimisation) is filled in.
- I know why the database is the first suspect.
Frequently asked questions
Why is clustering necessary, and is Node a bad choice for performance?
Clustering is necessary because Node runs your code on a single thread (one core), while modern servers have several: a single process wastes the other cores. Clustering (the cluster module, or PM2 which automates it with auto-restart and monitoring) launches several processes that share incoming requests. But that does not make Node a bad choice: Node excels at I/O (servers, APIs, real time — most web apps) with its non-blocking model, the single-thread limit concerns the CPU (manageable via worker threads), and Node scales well (vertical + horizontal). Used well, Node serves very large loads.
How do you know WHEN and WHAT to optimise without falling into premature optimisation?
Golden rule: do not optimise before a real, measured problem. Premature optimisation wastes time, complicates the code and misses the real bottleneck (intuition misleads). The approach: correct, clear code first, measure (profiling, metrics) when a problem occurs, optimise in a targeted way where it counts, re-measure to confirm. Common levers: database (indexes, N+1), cache (Redis), clustering, non-blocked loop, parallelism, horizontal scaling.
How does a Node application use all the cores of a server?