Every developer spends a large part of their time finding out why something doesn't work, or why it's slow. Guessing and sprinkling console.log everywhere works for small problems, but real applications need better tools. Node.js comes with a full debugger, built-in performance measurement, profilers for CPU and memory, and diagnostics that show whether the event loop is healthy.
Debugging is about understanding what the code actually does: pausing at a line, inspecting variables, and stepping through. Performance work is about measuring before changing anything, because the slow part is rarely where you expect. A route might be slow because of one unindexed database query, a loop that calls the database a hundred times, a large synchronous JSON operation, or memory slowly leaking until the process is restarted.
In this lesson you'll use breakpoints with --inspect, write structured logs, time code precisely, measure event-loop delay, find a memory leak, profile CPU usage and make a slow endpoint fast with caching. The habit to build is simple: measure, change one thing, measure again.
Debugging with the inspector
node --inspect server.js # start with the debugger listening
node --inspect-brk script.js # pause on the first line
node --watch --inspect server.js # restart and keep debugging on save- Open
chrome://inspectin Chrome, or use VS Code's Run and Debug panel (or its JavaScript Debug Terminal), to connect. - Set breakpoints by clicking line numbers, or write
debugger;in the code. - While paused, you can inspect variables, see the call stack, step over or into functions, and evaluate expressions in the console.
- Conditional breakpoints pause only when an expression is true, such as
order.total > 1000.
Logging that helps
console.errorandconsole.warnwrite to stderr;console.tableshows arrays of objects as a table;console.dir(obj, { depth: null })prints nested objects fully.- In production, write structured logs: one JSON object per line with a level, a message and useful fields such as
requestId,userIdanddurationMs. Libraries such as pino do this very efficiently. - Use levels (
debug,info,warn,error) and choose which to show with an environment variable. - Never log passwords, tokens, card numbers or other personal data.
Measuring performance
| Tool | What it shows |
|---|---|
performance.now(), console.time() | How long a piece of code takes |
monitorEventLoopDelay() from node:perf_hooks | How long callbacks wait to run, a sign of blocking |
node --cpu-prof app.js | A CPU profile to open in Chrome DevTools, showing which functions use the most time |
process.memoryUsage() | Heap and total memory in use |
Heap snapshots in DevTools, node --heapsnapshot-signal | Which objects fill memory, to find leaks |
| Load testing tools such as autocannon or k6 | Requests per second and response times under load |
Common fixes
- Cache results that are expensive and change rarely, in memory with an expiry (TTL) or in Redis when you run several instances.
- Avoid the N+1 query problem: one query for a list, then one more for each item. Load related data in a single query instead.
- Add database indexes for columns you filter or sort by.
- Stream large responses and compress them.
- Move CPU-heavy work to worker threads or background jobs.
- Don't
JSON.parseorJSON.stringifyenormous objects on the main thread in hot paths.
Node.js Advanced Syntax
Diagnostics toolbox
console.time('load'); await load(); console.timeEnd('load');
const t0 = performance.now(); /* work */ const ms = performance.now() - t0;
console.table(rows);
console.dir(obj, { depth: null });
debugger; // pauses when a debugger is attached
import { monitorEventLoopDelay } from 'node:perf_hooks';
const h = monitorEventLoopDelay(); h.enable(); /* later */ h.max / 1e6; // ms
process.memoryUsage().heapUsed;
// node --inspect app.js node --cpu-prof app.js npx autocannon http://localhost:3000Node.js Advanced Summary
- node --inspect connects Chrome DevTools or VS Code for breakpoints, stepping and inspecting variables.
- Use structured JSON logs with levels and request IDs; never log secrets.
- performance.now and console.time measure code; monitorEventLoopDelay reveals blocking.
- --cpu-prof and heap snapshots find slow functions and memory leaks.
- Fix bottlenecks with caching, fewer queries, indexes, streaming and worker threads, measuring each change.
Node.js Advanced Examples
Each example shows the code, the output it produces and what each part does.
Timing code and structured logs
function log(level, message, fields = {}) {
console.log(JSON.stringify({ level, message, ...fields }));
}
function slowSum(n) {
let total = 0;
for (let i = 0; i < n; i++) total += Math.sqrt(i);
return total;
}
const t0 = performance.now();
const result = slowSum(5_000_000);
const ms = performance.now() - t0;
log('info', 'Sum calculated', { result: Math.round(result), fast: ms < 2000 });
log('warn', 'Low stock', { product: 'mug', remaining: 2 });
console.table([
{ route: '/products', avgMs: 12 },
{ route: '/orders', avgMs: 87 },
]);{"level":"info","message":"Sum calculated","result":7453558807,"fast":true}
{"level":"warn","message":"Low stock","product":"mug","remaining":2}
┌─────────┬─────────────┬───────┐
│ (index) │ route │ avgMs │
├─────────┼─────────────┼───────┤
│ 0 │ '/products' │ 12 │
│ 1 │ '/orders' │ 87 │
└─────────┴─────────────┴───────┘Line-by-line explanation
- Lines 1 to 3Each log line is a single JSON object, easy for log tools to search and filter.
- Lines 11 to 13performance.now gives precise timings in milliseconds.
- Line 15Log the outcome; this demo prints a check instead of the exact time, which varies by machine.
- Lines 18 to 21console.table shows arrays of objects as a readable table.
Detecting a blocked event loop
import { monitorEventLoopDelay } from 'node:perf_hooks';
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
function blockFor(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {}
}
await new Promise((r) => setTimeout(r, 200));
const before = histogram.max / 1e6;
blockFor(300);
await new Promise((r) => setTimeout(r, 50));
const after = histogram.max / 1e6;
histogram.disable();
console.log(`Max delay while idle under 100 ms: ${before < 100}`);
console.log(`Max delay after blocking over 250 ms: ${after > 250}`);Max delay while idle under 100 ms: true
Max delay after blocking over 250 ms: trueLine-by-line explanation
- Lines 3 to 4The histogram measures how late timers fire, which reveals blocking.
- Lines 6 to 9A busy loop blocks the event loop, like a heavy synchronous calculation in a route.
- Line 12Values are in nanoseconds, so dividing by a million gives milliseconds.
- Lines 15 to 16After the block, the maximum delay jumps; production monitoring alerts on this.
Fixing a memory leak and adding a cache
const leakyCache = new Map();
function leakyLookup(id) {
if (!leakyCache.has(id)) leakyCache.set(id, { id, data: 'x'.repeat(1000) });
return leakyCache.get(id);
}
class TtlCache {
constructor(ttlMs, maxEntries) {
this.ttlMs = ttlMs;
this.max = maxEntries;
this.map = new Map();
}
get(key, now = Date.now()) {
const hit = this.map.get(key);
if (!hit || hit.expires < now) { this.map.delete(key); return undefined; }
return hit.value;
}
set(key, value, now = Date.now()) {
if (this.map.size >= this.max) this.map.delete(this.map.keys().next().value);
this.map.set(key, { value, expires: now + this.ttlMs });
}
}
for (let i = 0; i < 50_000; i++) leakyLookup(i);
console.log(`Leaky cache entries: ${leakyCache.size}`);
const cache = new TtlCache(60_000, 1000);
let dbCalls = 0;
function getProduct(id, now) {
const cached = cache.get(id, now);
if (cached) return cached;
dbCalls++;
const product = { id, name: `Product ${id}` };
cache.set(id, product, now);
return product;
}
for (let i = 0; i < 50_000; i++) getProduct(i % 200, 0);
console.log(`Bounded cache entries: ${cache.map.size}, database calls: ${dbCalls}`);
getProduct(5, 61_000);
console.log(`After expiry, database calls: ${dbCalls}`);Leaky cache entries: 50000
Bounded cache entries: 200, database calls: 200
After expiry, database calls: 201Line-by-line explanation
- Lines 1 to 5A cache with no limit and no expiry grows forever: a classic memory leak in long-running servers.
- Line 15Expired entries are removed when they're read.
- Line 19When full, the oldest entry is removed, so memory stays bounded.
- Line 3850,000 requests for 200 products need only 200 database calls.
- Lines 40 to 41After the time limit passes, the next request fetches fresh data.
Common Node.js Advanced Mistakes
Leaving console.log debugging in production code
console.log('HERE 2', user); // noisy, slow in hot paths, and may log personal datalogger.debug({ userId: user.id }, 'Loaded user'); // levelled, structured, no sensitive fieldsN+1 queries
const orders = await db.query('SELECT * FROM orders LIMIT 50');
for (const o of orders) o.customer = await db.query('SELECT * FROM customers WHERE id = $1', [o.customerId]); // 51 queriesconst orders = await db.query(`
SELECT o.*, c.name AS customer_name FROM orders o JOIN customers c ON c.id = o.customer_id LIMIT 50`); // 1 queryOptimising without measuring
// rewrote the template engine for speed; the real cost was an unindexed query// profiled with --cpu-prof and timed queries first, then added the missing indexNode.js Advanced in Real Projects
Observability in production
Production Node services send structured logs to tools like Datadog, Grafana Loki or CloudWatch, export metrics such as request duration and event-loop delay, and use OpenTelemetry tracing to follow a request across services. When something is slow, engineers look at traces to find the slow step, reproduce it locally, profile it with --cpu-prof or heap snapshots, fix it, and confirm the improvement with a load test.
Node.js Advanced FAQ
How do I fix "JavaScript heap out of memory" in Node.js?
First check whether the program really needs that much memory or is leaking it, for example by holding every request in an array or loading a huge file at once; take heap snapshots to find out. If it genuinely needs more, raise the limit with node --max-old-space-size=4096 app.js, where the number is megabytes. Streaming the data instead of loading it all is usually the better long-term fix.
How do I debug a Node.js app running in Docker?
Start Node with --inspect=0.0.0.0:9229 so the debugger listens on all of the container's network interfaces, publish the port with -p 9229:9229, and connect from chrome://inspect or VS Code. Only do this on your own machine: anyone who can reach the debugger port can run code in your process.
Why are my Node.js stack traces cut off?
V8 records only the last 10 call frames by default, so deep call chains lose their beginning. Raise the limit with node --stack-trace-limit=50 app.js or by setting Error.stackTraceLimit = 50 early in your code. Longer traces cost a little time each time an error is created, so keep the default in hot production paths.
What is a flame graph and how do I make one for Node.js?
A flame graph shows a CPU profile as stacked bars: each bar is a function, its width is how much time was spent in it and the functions it called, and the bars above it are its callees. Record a profile with node --cpu-prof app.js, then open the .cpuprofile file in Chrome DevTools' Performance panel, which can show it as a flame chart. Wide bars near the top are the first places to look.
Why is my Node.js server fast in testing but slow under load?
Under load, small problems add up. Common causes are synchronous work that blocks the event loop for every request, a database connection pool that's too small so requests queue for a connection, missing indexes that only hurt with real data volumes, and the N+1 queries that grow with list size. Measure event loop delay and database timings during a load test to see which it is.
Node.js Advanced Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the command that starts Node with the debugger listening.
node -- server.js
What does the debugger; statement do?
What does a high event-loop delay tell you?
What's the N+1 query problem?
Why does a cache need a size limit or expiry in a long-running server?
Node.js Advanced Quiz
5 questions to check what you learned.
Which method shows an array of objects as a table?
- console.dir
- console.table
- console.grid
- console.list
What is structured logging?
- Writing each log entry as data, such as one JSON object per line with fields
- Logging only errors
- Logging in a table
- Logging to separate files
Which flag records a CPU profile?
- --profile-cpu
- --inspect-cpu
- --cpu-prof
- --trace
What should you do before optimising code for speed?
- Rewrite it
- Add more servers
- Measure where the time actually goes
- Remove logging
Which tool helps find which objects are filling memory?
- console.time
- A heap snapshot in Chrome DevTools
- npm audit
- node --watch