We closed Module 9 with an uncomfortable sentence: Escena Viva is correct, but it is slow and wastes the machine. Every test passes, coverage is reasonable, and yet the server we rented has eight cores and we use one. On opening night of the Festival de Jazz de Primavera (evt-003, Auditorio Ribera, two sessions), when the sale opens and hundreds of people arrive at once, that single thread is the ceiling of the entire business.
This lesson attacks that waste with the most direct tool Node offers: the cluster module. We are going to measure first, understand the mechanism second, implement it in the project, and —very importantly— discover what breaks when an application designed for one process starts running in eight.
Contents
- The problem, measured: one thread, eight cores
- What
clusteris and how workers share a port - How many workers:
os.availableParallelism() - Implementation in Escena Viva:
src/cluster.js - Supervision: restarting workers without falling into a loop
- Inter-process communication (IPC)
- Zero-downtime reload
- What breaks: in-memory state stops being shared
- What cluster does NOT fix
child_processandfork, the module's cousins- Final measurement and the link to production
- The problem, measured: one thread, eight cores
In Module 2 we saw the architecture: V8 runs your JavaScript on a single thread, libuv contributes a thread pool of 4 threads for certain operations (fs, dns, crypto, zlib), and the rest of the I/O is delegated to the operating system. The practical consequence is simple arithmetic:
| Resource | Production machine | What one Node process uses |
|---|---|---|
| CPU cores | 8 | 1 for your JavaScript |
| Theoretical utilization | 100% | ~12.5% |
| libuv threads | — | 4, only for specific core tasks |
Before optimizing anything, we measure. That is the rule we will develop in lesson 10-04, but we apply it right now:
npm install --save-dev autocannon # load-testing tool
NODE_ENV=production node src/server.js # a single process, as until now
# In another terminal: 10 seconds, 50 concurrent connections, against the catalog
npx autocannon -c 50 -d 10 http://localhost:3000/api/eventsSummarized output on the reference machine (8 cores):
Stat 2.5% 50% 97.5% 99% Avg Stdev Latency 38ms 72ms 161ms 204ms 78.4ms 31.2ms Req/Sec 610 640 672 675 641
641 requests per second and a 99th percentile of 204 ms. Meanwhile, top shows one core at 100% and seven at zero. This number is our honest baseline: everything we do from now on is compared against it.
- What
cluster is and how workers share a port
cluster is and how workers share a portnode:cluster lets you launch several Node processes —workers— coordinated by a primary process, all of them able to serve the same port. Each worker is a complete process: its own V8, its own heap, its own event loop.
The obvious question is how eight processes can listen on port 3000 without the system returning EADDRINUSE. The answer depends on the policy:
- Default policy on Linux and macOS (
SCHED_RR, round-robin): only the primary process opens the socket and accepts connections. When one arrives, it picks a worker in rotation and hands it the file descriptor over the IPC channel. The distribution is balanced by construction. - Operating-system policy (
SCHED_NONE, the default on Windows): the primary creates the listening socket and shares it with all the workers; it is the system kernel that decides which process wakes up for each connection. It is marginally faster, but the distribution is usually very uneven: two or three workers end up absorbing almost all the traffic.
You choose with cluster.schedulingPolicy before forking, or with the NODE_CLUSTER_SCHED_POLICY variable (rr or none).
flowchart TD C[HTTP clients<br/>Festival de Jazz opening] --> P[Primary process<br/>accepts and distributes SCHED_RR] P -->|IPC: socket descriptor| W1[Worker 1<br/>startServer] P -->|IPC| W2[Worker 2<br/>startServer] P -->|IPC| W3[Worker 3<br/>startServer] P -->|IPC| W4[Worker N<br/>startServer] W1 --> DB[(PostgreSQL escena_viva)] W2 --> DB W3 --> DB W4 --> DB
Notice one detail in the diagram that will matter in section 9: the workers multiply, but the database is still a single one.
- How many workers:
os.availableParallelism()
os.availableParallelism()The naive answer is "as many as there are cores". The modern API for asking is os.availableParallelism() (Node 18.14+), which unlike os.cpus().length respects container limits and CPU affinity: if the orchestrator gives you 2 CPUs out of a 64-core machine, it returns 2, not 64.
'use strict';
const os = require('node:os');
// A sensible worker count based on the available parallelism.
// It can be forced through configuration for testing and profiling.
function calculateWorkerCount(requested) {
const available = os.availableParallelism();
if (Number.isInteger(requested) && requested > 0) {
return Math.min(requested, available * 2);
}
// Leave one core for the primary and the system when there is slack.
return available > 4 ? available - 1 : available;
}
module.exports = { calculateWorkerCount };Why it is not always every core:
| Situation | Recommended workers | Reason |
|---|---|---|
| Dedicated server, I/O-bound load | N or N−1 | The primary barely consumes CPU; leaving room for the system helps |
| Container with a CPU limit (M11) | Equal to the limit, rounded | More processes only add context switches |
| Scarce memory | Fewer than N | Each worker has its own heap: 8 × ~80 MB is not free |
| The database is already saturated | Fewer than N | More processes = more connections = more contention in the pool |
| Purely CPU-bound work | N (or threads, lesson 10-02) | Here the bottleneck is the computation, not the waiting |
- Implementation in Escena Viva:
src/cluster.js
src/cluster.jsThe key design decision is not to touch startServer(). In Module 6 we left src/app.js with the createApplication factory (which never calls listen) and src/server.js with startServer, which creates the http.Server, connects the database and handles graceful shutdown with SIGTERM/SIGINT and closeIdleConnections. That graceful shutdown, which at the time looked like a nicety, is exactly what makes the zero-downtime reload in section 7 possible.
'use strict';
const cluster = require('node:cluster');
const process = require('node:process');
const { configuration } = require('./config/index.js');
const { calculateWorkerCount } = require('./utils/parallelism.js');
const { startServer } = require('./server.js');
// Explicit scheduling policy: round-robin on every platform where it is
// available, so the load does not pile up on two processes.
cluster.schedulingPolicy = cluster.SCHED_RR;
// Window and limit of the restart circuit breaker.
const RESTART_WINDOW_MS = 60_000;
const MAX_RESTARTS_PER_WINDOW = 10;
const recentRestarts = [];
function recordRestart() {
const now = Date.now();
recentRestarts.push(now);
// Drop the restarts that fall outside the sliding window.
while (recentRestarts.length > 0 && now - recentRestarts[0] > RESTART_WINDOW_MS) {
recentRestarts.shift();
}
return recentRestarts.length;
}
function forkWorker() {
const worker = cluster.fork();
worker.on('message', (message) => handleWorkerMessage(worker, message));
return worker;
}
function handleWorkerMessage(worker, message) {
if (message && message.type === 'metrics') {
console.log(`[primary] ${worker.process.pid}: ${message.requests} requests`);
}
}
function startPrimary() {
const total = calculateWorkerCount(configuration.workers);
console.log(`[primary] pid ${process.pid}, forking ${total} workers`);
for (let i = 0; i < total; i += 1) forkWorker();
cluster.on('online', (w) => console.log(`[primary] ${w.process.pid} online`));
cluster.on('listening', (w, addr) => console.log(`[primary] ${w.process.pid} listening on ${addr.port}`));
cluster.on('disconnect', (w) => console.log(`[primary] ${w.process.pid} disconnected from IPC`));
cluster.on('exit', (worker, code, signal) => {
// Expected exit (reload or graceful shutdown): do not replace it.
if (worker.exitedAfterDisconnect) return;
const recent = recordRestart();
console.error(`[primary] ${worker.process.pid} died (${code}/${signal}); ${recent} in the window`);
if (recent > MAX_RESTARTS_PER_WINDOW) {
console.error('[primary] restart loop detected; waiting 30 s before replacing');
setTimeout(forkWorker, 30_000).unref();
return;
}
forkWorker();
});
// Graceful shutdown of the whole set: ask every worker to close.
for (const signal of ['SIGTERM', 'SIGINT']) {
process.on(signal, () => {
for (const worker of Object.values(cluster.workers)) {
worker.process.kill('SIGTERM');
}
});
}
}
async function main() {
if (cluster.isPrimary) {
startPrimary();
return;
}
// In the worker we reuse the usual startup, untouched.
await startServer();
}
main().catch((error) => {
console.error('Failed to start the cluster', error);
process.exitCode = 1;
});Points worth attention:
cluster.isPrimarydecides the role. The same file runs in all N+1 processes; forking re-executes the entry module from scratch.exitedAfterDisconnecttells an unexpected death (must be replaced) apart from a shutdown we asked for ourselves (must not be replaced). Without this check, the reload in section 7 conflicts with supervision.- The circuit breaker avoids the most painful pattern: a configuration failure (say, the database is down) makes every worker die on startup, the primary replaces it, it dies again... and the machine dedicates itself to forking processes at 500 Hz. With the sliding window, after 10 deaths in a minute we wait 30 seconds.
.unref()on the timer stops thatsetTimeoutfrom keeping the primary alive once everything else has finished.
We add the script "start:cluster": "node src/cluster.js" alongside the usual "start". And workers is read, like everything else, in src/config/index.js (the single place that touches process.env, frozen and validated).
- Supervision: restarting workers without falling into a loop
A worker's life cycle emits events worth telling apart:
| Event | When it fires | Typical use |
|---|---|---|
fork |
The primary has just created the process | Counters, startup time |
online |
The child process is already running JavaScript | Detect startups that never reach listening |
listening |
The worker has called listen |
The "it's ready" signal: key for the reload |
disconnect |
The IPC channel was closed | Intermediate phase of a graceful shutdown |
exit |
The process ended | Replace it if the exit was not expected |
The difference between online and listening is more than a nuance: a worker can be "online" and die afterwards because it cannot connect to PostgreSQL. Only listening guarantees that the process can serve traffic.
- Inter-process communication (IPC)
Every worker has a channel with the primary. From the worker you use process.send(message); from the primary, worker.send(message). Messages are serialized (JSON by default; with serialization: 'advanced' the structured clone algorithm is used, which we will see in 10-02).
In the worker, we publish metrics every 30 seconds:
'use strict';
const process = require('node:process');
const { monitorEventLoopDelay } = require('node:perf_hooks');
// We reuse the Module 2 histogram as a health measure for the process.
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
let requestsHandled = 0;
const countRequest = () => { requestsHandled += 1; };
function publishMetrics() {
if (typeof process.send !== 'function') return; // we are not in a cluster
process.send({
type: 'metrics',
requests: requestsHandled,
p99DelayMs: Number((histogram.percentile(99) / 1e6).toFixed(2)),
});
requestsHandled = 0;
histogram.reset();
}
setInterval(publishMetrics, 30_000).unref();
module.exports = { countRequest, publishMetrics };What IPC is really for:
- Aggregating metrics: each worker only knows its own share; the primary adds them up and exposes the total.
- Invalidating caches: if worker 3 sells tickets for
ses-003-1, it tells the primary and the primary relays the notice to everyone, so each one drops its copy of the catalog. It works, but it is a patch: the good solution is for the cache not to live in memory, and that is lesson 10-03. - Coordinating the reload: the primary orders, the worker obeys.
Its cost: every message is serialized, travels over a socket and is deserialized. Sending a small object every 30 seconds is irrelevant; sending the whole catalog on every sale is a design mistake. IPC is for control signals, not for sharing data.
- Zero-downtime reload
Deploying a new version by killing all eight workers at once means several seconds without service. The alternative is to restart them one at a time, waiting for the new one to be listening before touching the next.
'use strict';
const cluster = require('node:cluster');
// Replaces one worker with a fresh one without ever stopping traffic.
function replaceWorker(old, gracePeriodMs = 10_000) {
return new Promise((resolve, reject) => {
const replacement = cluster.fork();
replacement.once('listening', () => {
// The relief already accepts connections: now we can retire the old one.
old.disconnect(); // stop receiving new connections
// If the graceful shutdown does not finish in time, we force it.
const timer = setTimeout(() => old.process.kill('SIGKILL'), gracePeriodMs);
old.once('exit', () => { clearTimeout(timer); resolve(replacement); });
// The graceful shutdown already present in server.js does the rest:
// it closes the server, waits for in-flight requests and calls
// closeIdleConnections so it is not trapped by keep-alive.
old.process.kill('SIGTERM');
});
replacement.once('exit', (code) => {
if (code !== 0) reject(new Error(`The replacement died with code ${code}`));
});
});
}
async function reloadAll() {
// Sequential on purpose: doing it in parallel would drop capacity all at once.
for (const worker of Object.values(cluster.workers)) {
await replaceWorker(worker);
}
console.log('[primary] reload complete');
}
// Common convention: SIGHUP means "reload yourself".
process.on('SIGHUP', () => {
reloadAll().catch((error) => console.error('Reload failed', error));
});
module.exports = { replaceWorker, reloadAll };Two critical details. First, disconnect() before SIGTERM: this way the primary stops sending it new connections while it finishes the ones it has. Second, closeIdleConnections (M6) is indispensable: with keep-alive, a browser can hold an idle connection open for minutes and server.close() would never return.
- What breaks: in-memory state stops being shared
This is the key point of the lesson. Escena Viva worked with one process, and that process had memory. With eight processes there are eight memories, and everything we used to keep "in a variable" has been multiplied by eight without warning.
| Component | In-memory state | What happens with 8 workers |
|---|---|---|
src/services/currency-exchange.js |
Cache of rates with expiry | 8 caches: 8 calls to the external API where there was 1; up to 8 different rates at the same time |
src/middleware/session.js (express-session) |
In-memory session store | Lucía logs in on worker 2; her next request lands on worker 5 and she is logged out |
src/middleware/limits.js (express-rate-limit) |
Per-IP counters | purchaseLimit of 100 requests becomes 800 effective ones (100 per worker) |
SalesManager (the domain EventEmitter) |
In-process listeners | A low-capacity event emitted on worker 4 is heard by nobody else |
| Local counters and metrics | Module variables | Each one counts its slice; the total requires IPC |
Translated into real incidents from the Festival de Jazz opening:
- Intermittent sessions: Marc authenticates, browses, and suddenly the application asks him to log in again. It is not a sporadic failure: it happens on roughly 7 out of 8 requests. With JWT access tokens (M8) the problem is smaller, because the token is verified statelessly; but the opaque refresh token and CSRF do depend on the session.
- A useless rate limit: a script buying tickets at full speed gets up to 8 times the intended quota.
loginLimitstops being a defense against brute force. - Inconsistent capacity reads if anyone caches in memory: two users see different figures for the same capacity, because they are talking to different workers.
The general rule to internalize: an application that will run in several processes cannot keep shared state in its own memory. Shared state goes to an external store. That store, in lesson 10-03, will be Redis: connect-redis for sessions and rate-limit-redis for the limiter, and the catalog cache along the way.
In the meantime, there is one legitimate workaround if you need to deploy a cluster right now: sticky sessions (session affinity), where a load balancer always sends the same client to the same process. It is fragile (a worker that dies takes its clients' sessions with it), it prevents distributing the load properly and it fixes neither the rate limit nor the cache. It works as a bridge, not as a destination.
- What cluster does NOT fix
Cluster multiplies processes, and therefore multiplies the capacity to serve concurrent I/O. It performs no magic:
- A task that blocks the loop still blocks its worker. Generating the QR PDF for a ticket burns CPU synchronously. With 8 workers, a heavy generation blocks 1 in 8 instead of 1 in 1: you have gone from 100% unavailability to 12.5%, which is a statistical improvement, not a solution. If 8 PDF requests arrive at once, you are back where you started. The real solution is worker threads (10-02) and queues (10-03).
- The database is still shared. Eight workers with a pool of 10 connections each are 80 connections against PostgreSQL; if the server allows 100, you have just consumed 80% of the budget and any auxiliary process is left out. As you add workers you must reduce each one's pool size (we will see this in detail in 10-04).
- It does not fix bad algorithms. An N+1 query is still N+1 across eight processes: now eight times in parallel against the same database.
- It multiplies memory. Eight 100 MB heaps are 800 MB. On a tight machine, cluster causes swapping and makes latency worse.
child_process and fork, the module's cousins
child_process and fork, the module's cousinscluster is built on top of child_process.fork(), which launches a child Node process with a built-in IPC channel. The difference is that cluster adds the server socket distribution.
| API | What for | Shares a port | IPC channel |
|---|---|---|---|
cluster.fork() |
Scale an HTTP server to N processes | Yes | Yes |
child_process.fork() |
Launch an auxiliary Node process (nightly report, migration) | No | Yes |
child_process.spawn() |
Run an external binary with streams (M3) | No | No |
child_process.exec() |
A short shell command, capturing the output | No | No |
In Escena Viva we will use spawn in Module 11 for deployment tasks, and fork occasionally for auxiliary processes. A security warning inherited from M8: never build the exec command by concatenating user data; use spawn with an array of arguments.
- Final measurement and the link to production
We repeat exactly the same test, now with npm run start:cluster and 7 workers:
| Configuration | Req/s | p50 latency | p99 latency | Cores used |
|---|---|---|---|---|
| 1 process | 641 | 72 ms | 204 ms | 1 |
| 4 workers | 2,180 | 21 ms | 68 ms | 4 |
| 7 workers | 3,105 | 15 ms | 54 ms | 7 |
From 641 to 3,105 req/s: 4.8 times, not 7. The improvement is never linear, and the reasons are concrete:
- The primary burns CPU accepting and distributing connections (the
SCHED_RRpolicy). - The database starts to be the bottleneck: more workers do not speed up a slow query.
- There are shared resources: the network card, CPU caches, memory.
- Amdahl's law: the portion of the work that cannot be parallelized (here, the database) sets the ceiling.
And the honest conclusion: in production you will almost never write your own src/cluster.js. PM2 in cluster mode will do it, or the container orchestrator, the subject of Module 11. So why have we written it? Because PM2 does exactly this, and configuring it well requires understanding what it is doing: how many instances to ask for, what --wait-ready means, why a zero-downtime reload needs your graceful shutdown, and above all why your application must be ready to hold no state in memory before you scale it. That preparation is yours, not the supervisor's.
Common Mistakes and Tips
- Forking without controlling restarts. A startup error turns the primary into a fork bomb. A sliding window and a wait, always.
- Forgetting
exitedAfterDisconnect. Without it, every worker you retire on purpose is replaced immediately and the reload never finishes. - Putting business logic in the primary. The primary should be dumb: fork, supervise, distribute. If it does work, it becomes the new bottleneck.
- Sending large objects over IPC. Serialization eats up whatever you gained. Send identifiers and signals, not data payloads.
- Scaling to N workers without lowering the database pool size. It is the fastest way to take PostgreSQL down with "a performance improvement".
- Believing that cluster fixes CPU blocking. It spreads the damage; it does not remove it.
- Tip: add a
/healthendpoint that returnsprocess.pid. With repeatedcurlcalls you will see the distribution across workers and confirm at a glance thatSCHED_RRworks. - Tip: in development, start a single process. Debugging (M9) with eight processes and breakpoints is needlessly painful.
Exercises
Exercise 1: check the distribution and the circuit breaker
Add a GET /api/health route to Escena Viva that responds { pid, uptimeSeconds, worker: true }. Start with 4 workers and fire 20 consecutive requests, checking that the PIDs rotate. Then make one worker exit with process.exit(1) two seconds after starting and verify that the circuit breaker kicks in.
Exercise 2: demonstrate the loss of state
With the application running in a cluster (4 workers) and generalLimit configured to 20 requests per minute, write a script that fires 60 requests to /api/events from the same IP. Count how many get a 429. Explain the result and calculate the effective limit.
Exercise 3: aggregated metrics over IPC
Implement an aggregator in the primary that sums every worker's metrics and exposes them via GET /api/metrics. Since the primary does not serve HTTP, solve the problem: where does that endpoint live and how does it get the aggregated data?
Solutions
Exercise 1. The route goes in src/routes/index.js and is solved with process.pid, process.uptime() and Boolean(process.env.NODE_UNIQUE_ID) (a variable cluster injects into every worker). With for i in $(seq 1 20); do curl -s localhost:3000/api/health | jq -r .pid; done you will see 4 PIDs alternating cyclically: that is SCHED_RR at work. For the second part, a worker that dies repeatedly produces 10 restarts in under a second; from the eleventh on, the log shows "restart loop detected" and the primary waits 30 s. Without the circuit breaker, the process would burn an entire core forking.
Exercise 2. With 4 workers and SCHED_RR, the 60 requests are split into ~15 per worker. Since each one has its own counter and the limit is 20, no request gets a 429: the effective limit is 4 × 20 = 80 per minute. For it to be correct, you would need more than 80 requests. The practical conclusion is that the limiter has stopped protecting anything useful, and the only solid solution is a shared counter: rate-limit-redis in lesson 10-03. The workaround of dividing the limit by N (5 per worker) does not work well either, because the distribution is not perfectly uniform and it would penalize legitimate users.
Exercise 3. The endpoint cannot live in the primary if the primary does not listen on HTTP (and it should not: it would be the bottleneck). The clean solution is the other way round: the primary keeps the aggregate in memory and pushes it to the workers whenever it changes; each worker stores the last copy it received and serves it from its own /api/metrics route.
// In the primary: aggregate and broadcast.
const totals = new Map(); // pid -> last metric received
function handleWorkerMessage(worker, message) {
if (!message || message.type !== 'metrics') return;
totals.set(worker.process.pid, message);
const values = [...totals.values()];
const aggregate = {
type: 'aggregate',
requests: values.reduce((sum, m) => sum + m.requests, 0),
maxP99DelayMs: Math.max(...values.map((m) => m.p99DelayMs)),
workers: totals.size,
};
for (const other of Object.values(cluster.workers)) other.send(aggregate);
}And in the worker, process.on('message', ...) stores the latest aggregate in a variable that the route returns. It is a slightly stale answer (up to 30 s old), which is perfectly acceptable for metrics. The definitive version, with no IPC and no lag, is to keep the counters in Redis: the module's next stop, though not the immediate one.
Conclusion
Escena Viva no longer wastes seven out of every eight cores. We have honestly measured the baseline (641 req/s), understood that cluster distributes connections from a primary with the SCHED_RR policy, sized the workers with os.availableParallelism(), implemented src/cluster.js reusing startServer() without touching it, supervised the workers with protection against the restart loop, communicated between processes over IPC for control signals, and achieved zero-downtime reloads by leaning on the graceful shutdown from M6. The result: 3,105 req/s, 4.8 times more, and a p99 that drops from 204 to 54 ms.
We have also paid a price and put it in writing: in-memory state —the currency cache, sessions and the rate limit— has fragmented into eight copies, with real consequences for Lucía and Marc. And we have identified this tool's frontier: cluster does not unblock the event loop. When a worker settles down to compose the QR PDF for 500 Festival de Jazz tickets, that worker stops serving anyone.
That is exactly the subject of the next lesson: Worker Threads, where we will move CPU work off the main thread, measure event loop lag before and after, and build our own thread pool, because creating one thread per request is worse than not using threads at all.
Node.js Course: From Beginner to Advanced
Module 1: Introduction to Node.js
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
