Tests tell you that something is broken. They hardly ever tell you why. An integration test that expected 201 and got 500 has done you an enormous favor — it caught the bug before a customer did — but now you have to work out what happened inside that request: in which middleware, with what data, on which line.

This lesson closes the module with the other half of the craft: the tools and the method for diagnosing. One boundary before we start, so there is no confusion: here we diagnose what fails. Measuring and optimizing what works but runs slowly — CPU profiles, flamegraphs, load testing — is Module 10.

Contents

  1. From console.log to the debugger
  2. The built-in debugger: --inspect
  3. Debugging from VS Code, including Mocha tests
  4. Breakpoints and stepping
  5. Debugging asynchronous code
  6. Reading a real error stack trace
  7. Common errors and how to diagnose them
  8. Memory leaks
  9. Debugging in production without stopping the service
  10. A debugging method

From console.log to the debugger

Let's start with the obvious: console.log is nothing to be ashamed of. It is instant, it works in any environment, it needs no configuration and it often solves the problem in thirty seconds. What is true is that it forces you to modify the code, restart, and guess in advance what to print, and with nested objects it prints [Object] right where the answer was. First make the most of everything the console offers beyond log:

const util = require('node:util');

console.table(event.sessions.map((s) => ({   // comparisons you can read at a glance
  id: s.id, capacity: s.capacity, available: s.available,
  occupancy: `${(s.occupancy * 100).toFixed(1)} %`,
})));

console.dir(order, { depth: null, colors: true });   // the cure for [Object]
logger.debug(util.inspect(order, { depth: 4, breakLength: 120 })); // the same, as text

console.time('purchase');                   // how long a stretch takes
await buyTickets(data);
console.timeEnd('purchase');                // purchase: 187.42ms

console.trace('who is calling sell()');     // how we got here, without throwing
console.count('sale-recorded');             // how many times we pass through here
console.assert(session.sold <= session.capacity, 'OVERSALE', session.id);

console.dir(object, { depth: null }) is probably the most profitable trick on the list: by default Node prints only two levels of nesting, and with an order that contains lines that contain tickets those two levels run out immediately.

Even so, the debugger gives you three things the console cannot:

console.log Debugger
Deciding what to look at Before running During the run
Seeing the scope Only what you printed Every live variable
Call stack console.trace Navigable, with the scope of each frame
Modifying values No Yes, from the live console
Cost in production Pollutes the logs Zero when not active

The built-in debugger: --inspect

Node ships with a built-in debugger that speaks the Chrome DevTools protocol, and it is turned on with two flags:

node --inspect src/server.js       # starts and enables the inspector
node --inspect-brk src/server.js   # starts and STOPS on the first line
# Debugger listening on ws://127.0.0.1:9229/8f2a1c3d-...

The difference is decisive. --inspect is for debugging something that happens later — one specific HTTP request, an event: the server starts and waits. --inspect-brk is for debugging startup: reading the configuration, connecting to the database, mounting the middleware; without -brk, all of that has already happened before you can attach.

To connect from Chrome or Edge, open chrome://inspect, find your process under "Remote Target" and click inspect: DevTools opens with the Sources tab, where you can set breakpoints, and the Memory tab, which we will use for leaks. There is also a command-line debugger, node inspect src/server.js, useful over SSH where there is no browser: c continues, n steps one line, s steps into the function, o steps out, repl evaluates expressions. It is austere, but one day on a remote server it will save your afternoon.

Debugging from VS Code, including Mocha tests

This is the most productive way day to day. In .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Escena Viva server",
      "type": "node", "request": "launch",
      "program": "${workspaceFolder}/src/server.js",
      "envFile": "${workspaceFolder}/.env",
      "skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
      "console": "integratedTerminal"
    },
    {
      "name": "Mocha: the open file",
      "type": "node", "request": "launch",
      "program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
      "args": ["${relativeFile}", "--timeout", "0"],
      "envFile": "${workspaceFolder}/.env.test",
      "skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
      "console": "integratedTerminal"
    },
    {
      "name": "Attach to a running process",
      "type": "node", "request": "attach", "port": 9229, "restart": true,
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

The second configuration — you open orders.test.js, set a breakpoint and press F5 — is the one you will use most; just duplicate it without ${relativeFile} to debug the whole suite. The third is for when the process is already running with --inspect, for example inside a container.

Three details make all the difference. "--timeout", "0" is essential in the Mocha configurations: without it, Mocha aborts the test after 5000 ms while you are sitting there reading variables. skipFiles with node_modules and <node_internals> stops stepping from dragging you inside Express or Mongoose, so that "step into" takes you into your code. And envFile pointing at .env.test guarantees that you are debugging against the test database and not the development one.

Debugging tests is by far the most useful case: you have a bug that is deterministically reproducible, isolated, with known data, and you can stop it wherever you like. It is the ideal situation for debugging, and it is a gift the rest of this module gave you.

Breakpoints and stepping

There are three kinds of breakpoint, and only the first one ever gets used. The normal one stops on that line every time. The conditional one stops only when an expression holds, and it is the tool that turns an afternoon into five minutes: with the condition session.id === 'ses-001-1' && quantity > 4, in a loop over 7 sessions and 1811 sales you stop exactly on the case you are investigating (its variants are the hit count, which stops on call number 50, and the log expression). The logpoint stops nothing: it prints an interpolated message, Selling {quantity} of {session.id}, {session.available} left, every time execution passes by. It is a console.log that never touches the source code, perfect when the problem only appears under concurrency and stopping execution would make it vanish.

The controls are continue (F5, runs to the next breakpoint), step over (F10, runs the line without entering the functions it calls), step into (F11) and step out (Shift+F11, finishes the current function and returns to the caller).

And the four panels: Variables shows the local, closure and global scope at the point where you are stopped, and it is where you discover that req.body is undefined; Watch tracks specific expressions at every step (session.available, req.user.role); Call Stack reconstructs how you got here and you can click on any frame to see its variables, which is usually where the real bug lives; and the Debug Console evaluates any expression in the current context.

// Typed into the debug console, with execution paused:
session.available                        // 2
session.available >= req.body.quantity   // false  <-- here is the 409
req.user.role = 'administrator'          // change it live and continue to see what happens

That last line is an enormous capability: testing a hypothesis without editing, saving and restarting.

Debugging asynchronous code

In Node, half the problems happen after an await, and there debugging has a quirk. The stack breaks because when you register a callback with setTimeout or resolve a promise, the function runs later, on another turn of the event loop, and by then the original stack has been torn down. That is why a classic trace shows three frames (Timeout._onTimeout, listOnTimeout, processTimers), none of them useful: it does not say who scheduled that timeout.

Async stack traces are the solution. Node keeps information that allows the logical chain to be rebuilt across await boundaries, and DevTools and VS Code show it with separators:

Error: Insufficient capacity
    at Session.sell (/app/src/domain/session.js:58:11)
    at buyTickets (/app/src/services/orders.js:34:19)
--- await ---
    at createOrder (/app/src/controllers/orders.js:22:24)
--- await ---
    at Layer.handle (/app/node_modules/express/lib/router/layer.js:95:5)

The --- await --- markers separate the asynchronous stretches and read top to bottom as going backwards in time: the error was thrown in sell, called from buyTickets, which was awaited from createOrder.

Four practical tips. async/await produces far better traces than nested callbacks, one more reason to prefer it. Never write empty catch blocks: they erase the only clue you had. Preserve the cause when you rewrap an error, which is what makes a hierarchy like ours useful. And catch unhandled rejections at startup so that they never disappear silently:

try {
  await orderRepository.save(order);
} catch (error) {
  // The cause option chains the original trace: without it, you lose it
  throw new ApplicationError('Could not save the order', {
    appCode: 'PERSISTENCE_ERROR', cause: error,
  });
}

// src/server.js
process.on('unhandledRejection', (reason) => {
  console.error('Unhandled promise rejection:', reason);
  process.exit(1);   // fail fast: an unknown state is not safe
});

Reading a real error stack trace

A real trace is full of noise. Let's dissect one:

ValidationError: Order validation failed: lines.0.quantity: Path `quantity` (7)
is more than maximum allowed value (6).
    at model.Document.invalidate (/app/node_modules/mongoose/lib/document.js:3241:32)
    at /app/node_modules/mongoose/lib/schemaType.js:1368:9
    at process.processTicksAndRejections (node:internal/process/task_queues.js:77:11)
    at async orderRepository.create (/app/src/repositories/orders.js:47:20)
    at async buyTickets (/app/src/services/orders.js:61:19)
    at async createOrder (/app/src/controllers/orders.js:22:24)

You read it in this order. The type and the message first: a Mongoose ValidationError with the exact field (lines.0.quantity), the value received (7) and the rule broken (maximum 6); 80 % of the time the first line already says it all. Then skip the noise from node_modules and node:internal, because you are not going to fix Mongoose. Next find the first /app/src/ frame counting from the top — repositories/orders.js:47 — which is where your code triggered the error, and keep going down through your frames to rebuild the path: repository ← service ← controller. Diagnosis: somebody asked for 7 tickets and the zod validation did not stop them before they reached the model, so the real bug is not Mongoose's but a hole in the input validation, and the client is probably getting a 500 instead of the 422 they deserve.

Two tools for controlling traces. Node keeps 10 frames by default, which is short for long asynchronous chains: extend it with node --stack-trace-limit=50 or with Error.stackTraceLimit = 50. And Error.captureStackTrace(this, this.constructor) in the ApplicationError constructor removes the constructor's own frames from the trace, a small detail that makes your errors point straight at the code that threw them.

Common errors and how to diagnose them

All of these have already shown up over the course. This table is your quick reference:

Error Usual cause in Escena Viva How to diagnose it
ERR_HTTP_HEADERS_SENT A res.json() with no return followed by next(error) Breakpoint on the second send; look for the missing return
EADDRINUSE A forgotten npm run dev, or two tests with a fixed port lsof -i :3000 or ss -ltnp and kill the process; ephemeral port in tests
ECONNREFUSED Mongo or PostgreSQL stopped, or the wrong URI in .env Check the service and the variable; print configuration on startup
ETIMEDOUT A slow external provider, a firewall, a short AbortSignal.timeout Raise the time to confirm; then look at the network, not the timeout
MODULE_NOT_FOUND A forgotten extension, a wrong relative path, a stale node_modules The message carries the path it looked for: compare it character by character
unhandledRejection A forgotten await, or an async that is not propagated Register the process handler; the trace points to the spot
CastError GET /api/events/does-not-exist with Mongoose Validate the parameter with zod and return 400 or 404, not a 500
E11000 duplicate key Two users with the same email; the seed run without cleaning The message carries the index; translate it to StateConflict in the repository
The process never ends An unclosed Mongo connection, a live setInterval, a listening server why-is-node-running (below)
The request hangs The forgotten next() from Module 6 A logpoint in every middleware: the last one that prints is the culprit

The last two deserve more detail. With "exit": false in .mocharc.json, the process that never ends shows up the moment you forget to close something:

// test/helpers/setup.js, temporarily while you investigate
const whyIsNodeRunning = require('why-is-node-running');

exports.mochaHooks = {
  async afterAll() {
    await disconnect();
    setTimeout(() => whyIsNodeRunning(), 1000).unref();
  },
};

It prints every open handle and request with the stack trace of where it was created, and it is usually one of three things: the Mongoose connection, a setInterval from a cache or a token cleaner, or an http.Server created by hand. The alternative with nothing to install is printing process._getActiveHandles().length in an after: it is internal, undocumented API, but for debugging it does the job.

For the hanging request, the fastest diagnosis is a temporary tracing middleware, app.use(trace('json')), app.use(trace('authenticate')), and so on, where trace(label) prints [${req.requestId}] passing through ${label} and calls next(). The last trace that appears in the console points at the following middleware as the culprit.

Memory leaks

A leak is memory that is retained and no longer used. In a script it does not matter; in a server that runs for weeks, it ends in a restart for lack of memory. The symptoms: process memory grows steadily and does not come down during the quiet hours, performance degrades little by little because the collector works harder and harder, and eventually JavaScript heap out of memory arrives.

The first diagnosis needs no tools: a temporary setInterval that prints process.memoryUsage() every minute. Of its four fields, rss is the total process memory, heapTotal what V8 has reserved, external the Buffers, and heapUsed is the one that matters. The key is not the absolute value but the trend over hours: going up and down is normal, only going up is not.

The serious diagnosis is comparing two heap snapshots. Start with node --inspect, go to chrome://inspect, the Memory tab, and take snapshot A. Exercise the application by repeating the same cycle several times — purchases, listings, logins — and take snapshot B. Select B and switch the view to Comparison against A: the Delta column shows which object types have grown without being freed, and if after ten identical cycles you see 10,000 more Session objects or 500 more listeners, there is your leak. The Retainers section tells you who is keeping the reference alive, which is the information you actually need. You can also generate one from code with v8.writeHeapSnapshot(), useful on a server with no browser: it returns the path of a .heapsnapshot file that you then download and load into DevTools.

The typical causes, all of them already seen in this course:

Cause How it happens in Escena Viva Fix
Listeners never removed (M2) Every request does salesManager.on('sale-recorded', ...) and nobody calls off Use once or an explicit off; watch for MaxListenersExceededWarning
An unbounded cache (M4) The currency-exchange.js cache stores every currency and never expires A size limit (LRU) and a per-entry expiry
Module variables that accumulate A "recent requests" array that only ever gets pushed A maximum size, or move it to Redis (Module 10)
Closures that retain A callback that captures the whole req and lives in a timer Capture only what you need (req.requestId, not req)
Timers never cancelled A setInterval per user session clearInterval when done; unref() where appropriate

The warning MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 sale-recorded listeners added is a gift from Node: it is telling you exactly where to look. Do not silence it by raising setMaxListeners; investigate why they are being added.

Debugging in production without stopping the service

In production you cannot set a breakpoint: you would freeze every user's requests. The techniques are different.

1. Structured logs with a request identifier. This is by far the most valuable tool. Module 6's request-id.js middleware assigns a unique identifier to every request and request-logger.js includes it in every line:

{"level":"info","requestId":"9f3c1e","method":"POST","path":"/api/orders","userId":"usr-001","message":"purchase started"}
{"level":"warn","requestId":"9f3c1e","code":"INSUFFICIENT_CAPACITY","sessionId":"ses-001-1","available":2,"requested":4}
{"level":"info","requestId":"9f3c1e","status":409,"durationMs":91}

With that requestId you reconstruct the full journey of one specific request among thousands of simultaneous lines: when Lucía writes in saying she could not buy at 18:22, you have her whole story by filtering on one field. Monitoring and aggregating these logs is wrapped up in Module 11. Golden rule: never log personal data, passwords, tokens or Authorization headers, because a log is a file many people can read and one that is often shipped to a third party.

2. Turning the inspector on with a signal, temporarily. kill -USR1 <pid> makes Node open the inspector on 9229 without restarting the process, and you connect through an SSH tunnel: ssh -L 9229:localhost:9229 user@server, and then chrome://inspect on your local machine sees the remote process.

3. Never leave --inspect open on a public server. This is a serious security warning, not a style recommendation: the inspector port has no authentication, so anyone who reaches it can evaluate arbitrary code in your process, read memory — including the JWT secrets and the database passwords — and change the behavior live. It is remote code execution served on a platter. The minimum rules: never --inspect=0.0.0.0:9229 (by default it listens only on 127.0.0.1, leave it that way), always go through an SSH tunnel, close it as soon as you are done, and have the firewall block 9229 from outside as a safety net.

4. Heap dumps on demand, through an internal endpoint protected by the administrator role and reachable only from the private network, calling v8.writeHeapSnapshot(). It lets you investigate a leak in production without touching the service.

It is worth stating the boundary clearly, because the confusion is common:

Debugging (this module) Profiling (Module 10)
Question Why is it doing the wrong thing? Why is it so slow?
Symptom An error, a wrong result, a hang Slowness, low throughput
Tools Inspector, breakpoints, stack traces, logs CPU profiles, flamegraphs, autocannon, clinic
Outcome A located bug and a test that reproduces it A quantified bottleneck

A debugging method

Tools without a method produce wasted afternoons. This is the procedure, in order.

1. Reproduce. A bug you cannot reproduce is a bug you cannot fix; at best you can change things until it seems to go away. Gather the exact request, the user and role, the data involved, the time and the requestId; if it is intermittent, run it in a loop until it drops.

2. Isolate. Reduce to the smallest case that still fails: does it fail without authentication? with another session? only with evt-002? Every answer clears half the forest.

3. Form a falsifiable hypothesis. Not "something weird is going on with capacity", but "I think sell() compares with < instead of <= and that is why it rejects the last ticket". A hypothesis that cannot be refuted is worthless.

4. Test it with a breakpoint, a logpoint or a query. If it was false, do not twist it: form another. Clinging to a wrong hypothesis is the number one cause of long debugging sessions.

5. Bisect with git bisect when you know it used to work:

git bisect start
git bisect bad                    # the current commit fails
git bisect good v1.4.0            # this version worked
git bisect run npm run test:unit  # automatic: Git uses the exit code
git bisect reset

With 1000 commits between the good one and the bad one, git bisect finds the culprit in 10 steps, and with run you go for a coffee and come back with the exact commit. Notice what that implies: automatic git bisect only works if you have tests. It is one more benefit of this module that you do not see until you need it.

6. Write a test that reproduces the bug, before fixing it. This step closes the circle of the entire module and is non-negotiable. The order matters: you write the test and it must fail — if it passes, you have not understood the bug and you are about to fix something else; then you fix the code, the test passes, and finally you run the whole suite to check that the fix did not break anything else.

// Incident 481: an order for exactly the last remaining seats returned 409.
// This test was red before the fix (sell compared with < instead of <=).
it('allows buying exactly the last available seats', async () => {
  const lucia = await asAttendee();
  await adjustCapacity('ses-001-1', { available: 2 });

  const { body } = await request(app).post('/api/orders').set(...lucia.header)
    .send({ sessionId: 'ses-001-1', quantity: 2 }).expect(201);
  expect(body.tickets).to.have.lengthOf(2);

  const session = await request(app).get('/api/sessions/ses-001-1').expect(200);
  expect(session.body.available).to.equal(0);
  expect(session.body.soldOut).to.be.true;
});

You gain three things from that test: you prove you understood the bug, you guarantee it will never come back, and you leave documented an edge case nobody had thought of. A production bug is expensive; wasting it without turning it into a test is throwing money away.

Common Mistakes and Tips

  • Debugging without reproducing. Changing code until "it seems fine now" fixes nothing: it hides the problem until the worst possible moment.
  • Forgetting --timeout 0 when debugging tests. Mocha aborts the test while you are stopped reading variables and you think the debugger is broken.
  • Not using skipFiles. You press F11 and end up inside express/lib/router/layer.js with no idea how to get out.
  • Empty catch blocks. They erase the only information you had; always log, even at debug level.
  • Losing the cause when rewrapping errors. Use { cause: error }: the original trace is worth more than a pretty message.
  • Silencing MaxListenersExceededWarning. It is a free leak detector; raising the limit is covering up the fire alarm.
  • Leaving --inspect on a reachable server. It is remote code execution with no authentication. Always use an SSH tunnel.
  • Logging tokens or passwords. A log is a file many people read and one that usually leaves your infrastructure.
  • Tip: when an integration test fails with a 500, print response.body and look for the requestId in the server output: you have the whole story in two steps. And learn conditional breakpoints well, the skill that saves the most time in the whole lesson.

Exercises

Exercise 1: diagnose without running anything

For each symptom, state the most likely cause, the tool you would confirm it with and the fix:

  1. npm test prints 74 passing and the process hangs without returning the prompt.
  2. POST /api/orders answers 500 with Cannot set headers after they are sent to the client.
  3. GET /api/events/hello returns 500 instead of 404.
  4. The server has been up for five days and heapUsed has gone from 90 MB to 780 MB.
  5. An integration test passes on its own and fails inside the full suite.

Exercise 2: debug a test with VS Code

Take the concurrency test from 09-04. Configure .vscode/launch.json with the "Mocha: the open file" entry, place a conditional breakpoint inside the orders repository that only triggers when available < 2, and describe what you would see in the call stack panel and in the variables panel. Explain why --timeout 0 is essential here.

Exercise 3: from bug to test

An organizer at Sala Bóveda reports that the report for evt-002 has been showing revenueCents: NaN since yesterday. Design the whole process: how to reproduce it, how to isolate it, two falsifiable hypotheses, how you would use git bisect, and write the test that reproduces the bug (it must fail before the fix).

Solutions

Exercise 1. (1) An open handle, almost certainly the unclosed Mongo connection; confirm it with why-is-node-running in afterAll and fix it with await disconnect() in the root hook. (2) A double response: a handler calls res.json(...) and then next(error), or there is an await after responding that throws; put a breakpoint in errorHandler and look at the stack; the fix is return res.json(...). (3) A Mongoose CastError: hello is not a valid ObjectId and the error arrives as unknown, which STATUS_BY_CODE translates to 500; validate the parameter with zod or translate CastError to ResourceNotFound in the repository. (4) A memory leak: two heap snapshots compared after identical cycles, with the listeners accumulating on SalesManager and the unbounded currency-exchange.js cache as the main suspects. (5) Contamination between tests: an unrestored stub, a live fake clock or data another test left behind; confirm it by running the suite in reverse order and fix it with sinon.restore() in the root hook and cleanup in afterEach.

Exercise 2. The conditional breakpoint goes on the repository line where capacity is checked, with the condition session.available < 2. In the call stack panel you would see, top to bottom, the repository method, buyTickets, the createOrder controller and several Express frames separated by --- await --- markers; clicking on the controller frame would show you req.user and req.validatedData for that specific request. In the variables panel you would see session.available with its exact value at the instant when one of the ten simultaneous purchases finds the capacity nearly exhausted, and you could check in the console whether the comparison against quantity gives what you expected.

--timeout 0 is essential for a specific reason: the test fires ten requests with Promise.all, and while you are stopped on one the other nine keep waiting; with the normal 5000 ms timeout, Mocha would abort the entire test after five seconds, tearing the application down under your feet.

Exercise 3. Reproduce: call GET /api/events/evt-002/report as an org-boveda organizer against the seeded test database; if it does not reproduce, copy the real evt-002 data, because then the bug is in the data and not in the code. Isolate: try evt-001 and evt-003; if only evt-002 fails — the one with three sessions — compute the report session by session to find which one produces NaN. Falsifiable hypotheses: that one session has a null priceCents and the reduce propagates NaN (because undefined * 2 is NaN and NaN poisons the whole sum), or that the report adds up priceEuros instead of priceCents. Bisection: since "since yesterday" narrows the window, git bisect start, bad HEAD, good <the commit from the day before yesterday> and git bisect run npm run test:unit.

it('computes the revenue even when a session has no price assigned', () => {
  const event = createEvent({
    id: 'evt-002',
    sessions: [
      { sold: 120, priceCents: 1800 },
      { sold: 95, priceCents: 1800 },
      { sold: 60, priceCents: undefined },   // the problematic session
    ],
  });
  expect(Number.isNaN(event.revenueCents)).to.be.false;
  expect(event.revenueCents).to.equal(387000);  // (120 + 95) * 1800
});

This test is red before the fix, which has two parts and both matter: treating the missing price as zero in the calculation (s.priceCents ?? 0) and making the model require priceCents, so that the invalid data cannot get in again. Fixing only the symptom would leave the door open.

Conclusion

This lesson closes Module 9, and with it closes a gap that had been open since Module 5.

Escena Viva no longer rests on trust. npm test is green and it means something: the domain computes capacity and totals in cents correctly, the permission matrix is walked cell by cell — that inverted if in canManageEvent that would open the whole catalog now turns several tests red — the currency converter degrades gracefully when the provider goes down, the API answers 401, 403, 404, 409, 422 and 400 in the agreed error format, one attendee cannot see another's order, and ten simultaneous purchases for five seats sell exactly five seats. Coverage is measured with thresholds that only go up, the scripts are organized, and the project can run on its own on a continuous integration server. And when something fails — because something will fail — you know how to reproduce it, isolate it, put a conditional breakpoint on it, read its stack trace through the node_modules noise, find the guilty commit with git bisect and turn it into a test that prevents its return.

That is the safety net. From now on you can change the code without fear, which was exactly the promise we opened the first lesson with.

And yet the application has a problem the tests say nothing about, because it is not a correctness problem: it is correct, but it is slow and it wastes the machine. Escena Viva uses a single core while the server has eight, and on opening night for the Festival de Jazz de Primavera every process will be fighting over that one thread. It recomputes the same catalog hundreds of times a minute to return exactly the same thing. And when it generates the PDF for a ticket, it blocks the event loop long enough for the other requests to sit waiting in the queue.

In Module 10, Advanced Topics, we attack exactly that: cluster to use every core, worker threads to move PDF generation off the main thread, Redis to cache the catalog and queue the heavy work, performance optimization with CPU profiles, flamegraphs and load tests — the boundary we have respected in this module — and finally the design of mature RESTful APIs and an introduction to GraphQL.

Node.js Course: From Beginner to Advanced

Module 1: Introduction to Node.js

Module 2: Core Concepts

Module 3: File System and I/O

Module 4: HTTP and Web Servers

Module 5: NPM and Package Management

Module 6: The Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2026. All rights reserved