Debugging & Profiling Node
Reviewed & published by Brayan K
Debugging is finding and fixing why your code misbehaves, and profiling is measuring where it spends time and memory — Node gives you the inspector, the debugger keyword, console timers, and CPU and heap profilers to do both.
Part of the free Node.js course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll measure code with console.time, read memory with process.memoryUsage(), set breakpoints with --inspect and the debugger keyword, profile CPU with --prof and clinic.js, and hunt memory leaks with heap snapshots.
What You'll Learn in This Lesson
- Timing code blocks with console.time / timeEnd
- Reading memory usage with process.memoryUsage()
- Debugging with node --inspect and Chrome DevTools
- Setting a breakpoint with the debugger keyword
- CPU profiling with --prof and clinic.js
- Finding memory leaks with heap snapshots
1️⃣ Timing Code with console.time
The fastest way to measure a block is a pair of timers. console.time('label') starts a stopwatch and console.timeEnd('label') stops it and prints the elapsed time. Use the same label for both. It's zero-setup and perfect for confirming whether a suspected hotspot is actually slow.
// console.time / console.timeEnd measure how long a block takes.
// You give the timer a label and end it with the same label.
console.time('sum');
let total = 0;
for (let i = 0; i < 1_000_000; i++) total += i;
console.timeEnd('sum'); // prints: sum: <duration>ms
console.log('Total:', total);2️⃣ Reading Memory Usage
process.memoryUsage() returns an object of byte counts. The two you watch most are rss (total resident memory) and heapUsed (live JavaScript objects). Sampling it before and after work shows how much an operation costs. Watching heapUsed climb without falling is the classic signature of a memory leak.
// process.memoryUsage() reports how much memory your process is using.
// rss = total resident memory, heapUsed = live JS objects. Values are BYTES.
const before = process.memoryUsage().heapUsed;
// Allocate a big array to move the numbers.
const big = new Array(1_000_000).fill('x');
const after = process.memoryUsage().heapUsed;
const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1) + ' MB';
console.log('Heap before:', mb(before));
console.log('Heap after: ', mb(after));
console.log('Array kept alive so it is not garbage-collected:', big.length);3️⃣ The Inspector, Breakpoints & Profilers
For real debugging, start your app with node --inspect app.js and open chrome://inspect to get full DevTools — breakpoints, stepping, variable inspection. The debugger keyword drops a breakpoint right in your code. For performance, node --prof (or clinic.js) profiles CPU, and the Memory tab's heap snapshots reveal leaks.
// 1) The interactive debugger — pause, step, inspect variables.
// Start your app with the inspector enabled:
// node --inspect app.js (or --inspect-brk to break on line 1)
// Then open chrome://inspect in Chrome and click "inspect".
// 2) The debugger keyword sets a breakpoint in code.
function risky(x) {
debugger; // execution PAUSES here when run under --inspect
return x.toUpperCase(); // crashes if x is not a string — inspect x here
}
// 3) CPU profiling — find the slow functions.
// node --prof app.js (writes an isolate-*.log)
// node --prof-process isolate-*.log > profile.txt
// Or use clinic.js for friendly flame charts:
// npx clinic doctor -- node app.js
// 4) Memory leaks — take heap snapshots in chrome://inspect's Memory tab,
// do some work, take another, and compare what GREW between them.Your turn. The timer below works once you fill in the single blank marked ___. Follow the 👉 hint, then run it (your exact ms will differ, but the value won't).
// 🎯 YOUR TURN — replace each ___ then run it.
console.time('work');
let product = 1;
for (let i = 1; i <= 10; i++) product *= i; // computes 10!
console.___('work'); // 👉 end the SAME-labeled timer: timeEnd
console.log('10! =', product);Mini-Challenge: Profile Two Approaches
No blanks — just a brief and an outline. Use two labeled timers to compare string concatenation versus array-join for building a big string. Build it, run it, and see which wins on your machine (the exact times vary).
// 🎯 MINI-CHALLENGE: profile two approaches with timers
// 1. withConcat(n): build a string by += 'x' n times; return its length.
// 2. withArray(n): push 'x' into an array n times, join it; return the length.
// 3. Wrap each call in console.time/timeEnd with its own label and run both
// with n = 100000, printing each length.
// (The exact ms vary by machine, but you'll see which approach is faster.)
//
// ✅ Example output (durations will differ on your machine):
// concat length: 100000
// concat: 14.1ms
// array length: 100000
// array: 4.5ms
// your code hereCommon Errors (and the fix)
- ❌ Mismatched console.time labels — timeEnd('a') after time('b') prints a warning and no time. ✅ Use the exact same label for both.
- ❌ Leaving a debugger statement in committed code — it pauses anyone running under an inspector. ✅ Remove it before you commit.
- ❌ Reading memory numbers as bytes vs MB — memoryUsage() returns bytes. ✅ Divide by 1024*1024 for MB.
- ❌ --inspect can't connect — the inspector port is firewalled or already in use. ✅ Check the port (default 9229) and that nothing else holds it.
- ❌ Chasing a "leak" that's just normal growth — memory rises then GC reclaims it. ✅ Confirm with two heap snapshots that retained memory keeps growing and never drops.
Pro Tips
- 💡 Measure before you optimize: profile to find the real hotspot — intuition about what's slow is often wrong.
- 💡 Prefer the debugger over console.log: breakpoints let you inspect live state without editing and re-running.
- 💡 Use clinic.js for a friendly start: clinic doctor flags whether your bottleneck is CPU, I/O, or the event loop.
📋 Quick Reference — Debug & Profile
| Task | Tool |
|---|---|
| Time a block | console.time/timeEnd('x') |
| Read memory | process.memoryUsage() |
| Interactive debug | node --inspect app.js |
| Inline breakpoint | debugger; |
| CPU profile | node --prof app.js |
| Friendly profiler | npx clinic doctor |
| Find leaks | heap snapshots (Memory tab) |
🎉 Lesson Complete!
- ✅ console.time/timeEnd measure how long a block takes
- ✅ process.memoryUsage() reports heap and total memory in bytes
- ✅ node --inspect + Chrome DevTools gives breakpoints and stepping
- ✅ The debugger keyword sets a breakpoint in code
- ✅ --prof and clinic.js profile CPU to find slow functions
- ✅ Heap snapshots compared over time reveal memory leaks — measure before optimizing
Practice quiz
What does console.time('x') paired with console.timeEnd('x') measure?
- How much memory a block uses
- How long the block between them takes to run
- How many times a function is called
- The CPU temperature
Answer: How long the block between them takes to run. console.time/timeEnd start and stop a stopwatch for the code between them and print the elapsed time.
Why must console.time and console.timeEnd use the same label?
- Labels are optional and ignored
- Mismatched labels print a warning and no timing
- The label sets the units to ms
- Different labels run the timer twice as fast
Answer: Mismatched labels print a warning and no timing. timeEnd must match the label given to time; a mismatch prints a warning and no duration.
What units does process.memoryUsage() report values in?
- Megabytes
- Kilobytes
- Bytes
- Gigabytes
Answer: Bytes. All values are in bytes; divide by 1024*1024 to convert to megabytes.
Which field of process.memoryUsage() tracks live JavaScript objects?
- heapUsed
- rss
- external
- arrayBuffers
Answer: heapUsed. heapUsed is the size of live JS objects; rss is total resident memory.
How do you start an app with the inspector so you can use Chrome DevTools?
- node --debug app.js
- node --profile app.js
- node --watch app.js
- node --inspect app.js
Answer: node --inspect app.js. node --inspect app.js enables the inspector; open chrome://inspect to attach DevTools.
What does the debugger keyword do when run under an inspector?
- It prints the call stack to the console
- It pauses execution at that line as a breakpoint
- It restarts the program
- It disables all other breakpoints
Answer: It pauses execution at that line as a breakpoint. The debugger statement is an inline breakpoint that pauses execution when an inspector is attached.
Which command does CPU profiling to find slow functions?
- node --mem app.js
- node --prof app.js
- node --trace app.js
- node --slow app.js
Answer: node --prof app.js. node --prof writes an isolate log you process into a profile that shows where CPU time goes.
What is the classic signature of a memory leak?
- heapUsed that climbs over time and never comes back down
- A single spike that the GC reclaims
- Constant memory that never changes
- CPU usage at 100 percent
Answer: heapUsed that climbs over time and never comes back down. A leak shows as retained memory that keeps growing and is never reclaimed, risking an out-of-memory crash.
When should you reach for a profiler instead of console.time?
- When you already know the exact slow line
- When you don't yet know where the time goes across the whole program
- When you only need to format output
- When the program has no functions
Answer: When you don't yet know where the time goes across the whole program. Use console.time to confirm a known hotspot; use a profiler to discover an unknown one across the whole app.
How do you compare two heap snapshots to find a leak in Chrome DevTools?
- Run --prof-process twice
- Use console.memory() before and after
- Take a snapshot, do work, take another, and see which object types grew
- Restart the process between reads
Answer: Take a snapshot, do work, take another, and see which object types grew. Two snapshots taken around the suspect workload let the Comparison view reveal which retained objects grew.
Continue this course
- Previous: Publishing npm Packages
- Next: Checkpoint: Production Backend — Combine caching, rate limiting and scaling in a build — then a quiz
- Quick reference: Node.js cheat sheet
Frequently asked questions
How do I use node --inspect with Chrome DevTools?
Run your program with node --inspect app.js (or node --inspect-brk app.js to pause before the first line), then open chrome://inspect in Chrome and click the 'inspect' link under Remote Target. You get the full DevTools experience for Node: set breakpoints by clicking line numbers, step through code, hover to inspect variables, watch expressions, and read the call stack — exactly like debugging front-end JavaScript. This is far more powerful than scattering console.log calls because you can pause live execution and explore the actual state. Many editors (VS Code in particular) wrap this same inspector protocol in a built-in debugger UI, so you can also debug without leaving your editor.
What does the debugger keyword do?
The debugger statement is a breakpoint written directly in your code. When the program runs under an inspector (node --inspect with DevTools or your editor's debugger attached), execution pauses on that line so you can inspect variables, step forward, and see the call stack at that exact moment. If no debugger is attached, the statement does nothing and execution continues, so it's safe to leave temporarily — though you should remove it before committing. It's handy for pausing deep inside a specific branch you can't easily reach by clicking, but for permanent breakpoints the editor/DevTools UI is cleaner since it doesn't change your source.
When should I use console.time versus a profiler?
console.time('label') and console.timeEnd('label') are perfect for a quick, targeted measurement: you suspect one block or function is slow, so you wrap it and read the duration. It's zero-setup and great for comparing two implementations. A profiler (node --prof, or a friendlier tool like clinic.js) is what you reach for when you don't yet know where the time goes — it samples the whole program and tells you which functions consume the most CPU, often shown as a flame chart. Rule of thumb: console.time to confirm a hypothesis about a known hotspot, a profiler to discover an unknown one across the whole app.
How do I find a memory leak in Node?
A memory leak shows up as memory (especially heapUsed from process.memoryUsage(), or rss) that climbs over time and never comes back down, eventually risking an out-of-memory crash. To track it down, run under node --inspect, open the Memory tab in Chrome DevTools, and take a heap snapshot; perform the suspect workload (for example handle many requests); take a second snapshot; then use the 'Comparison' view to see which object types GREW and were retained between the two. The objects that keep accumulating — often things pushed into a long-lived array, cache, or event listener that's never removed — point you at the leak. Common culprits are forgotten timers, growing global caches, and listeners added but never removed.