Home JavaScript Debugging Node.js Applications — Inspector, Chrome DevTools, and VS Code
Intermediate 6 min · 2026-07-12

Debugging Node.js Applications — Inspector, Chrome DevTools, and VS Code

Node.js debugging mastery: Chrome DevTools inspector, VS Code debugger, CPU profiling, heap snapshots, async stack traces, and diagnosing memory leaks in production..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Node.js includes a built-in inspector protocol accessible via --inspect and --inspect-brk flags. The Chrome DevTools debugger attaches to this protocol and provides step-through debugging, console pro

✦ Definition~90s read
What is Debugging Node.js Applications?

Node.js includes a built-in inspector protocol accessible via --inspect and --inspect-brk flags. The Chrome DevTools debugger attaches to this protocol and provides step-through debugging, console profiling, heap snapshots, CPU profiling, and async stack traces.

Debugging Node.js is like being a detective in a busy kitchen.

VS Code's built-in debugger wraps the inspector protocol with launch.json configuration. For production debugging, Node.js 22 supports heap snapshot generation without stopping the process (--heapsnapshot-signal), CPU profiling via the inspector/profiler module, and async stack traces that preserve the full async/await call chain.

Plain-English First

Debugging Node.js is like being a detective in a busy kitchen. You have a recipe (your code), but the dish isn't coming out right. The Inspector is your magnifying glass—it lets you pause the action, peek at ingredients (variables), and step through each cooking step (line of code). Chrome DevTools is your surveillance camera showing real-time kitchen activity. VS Code is your detective's notebook where you can set breakpoints like 'stop here and check the pot.' Without these tools, you're just guessing which ingredient is spoiled.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

console.log debugging works until your server handles 500 requests per second and you realise logging every variable doubles response time. Node.js ships with a production-grade debugger that most developers never open. The inspector protocol gives you heap snapshots, CPU flame graphs, and step-through debugging without any third-party tools. This article covers the full debugging workflow — from attaching the inspector to diagnosing memory leaks with heap comparisons — using only tools shipped with Node.js and VS Code.

Why Node.js Debugging Is Different

Debugging Node.js is not like debugging client-side JavaScript. You don't have a browser DOM, and your code runs on a server with limited visibility. Traditional console.log debugging works for trivial cases but fails in production where you need to inspect async flows, memory leaks, or race conditions. Node.js provides a built-in inspector protocol that allows external tools to attach and control execution. Understanding this protocol is the foundation for effective debugging. The inspector exposes endpoints for breakpoints, stepping, profiling, and heap snapshots. Tools like Chrome DevTools and VS Code use this protocol under the hood. Without this knowledge, you're flying blind when things break in production.

start-inspector.shBASH
1
2
node --inspect app.js
# Output: Debugger listening on ws://127.0.0.1:9229/...
Output
Debugger listening on ws://127.0.0.1:9229/...
🔥Inspector Protocol
The --inspect flag starts the inspector on port 9229 by default. Use --inspect-brk to pause on the first line.
📊 Production Insight
In production, never leave --inspect enabled. It opens a debugging port that attackers can exploit. Use it only in staging or with proper firewall rules.
🎯 Key Takeaway
Node.js debugging relies on the inspector protocol — learn it to debug effectively.
debugging-nodejs THECODEFORGE.IO Node.js Debugging Architecture Layered components from runtime to debugging tools Application Layer Node.js Process | Async Code | Promises Inspector Protocol WebSocket | Chrome DevTools Protocol Debugging Tools Chrome DevTools | VS Code Debugger | CLI Inspector Profiling Layer CPU Profiler | Heap Snapshot | Memory Analyzer Production Support --inspect Flag | Port Forwarding | Security THECODEFORGE.IO
thecodeforge.io
Debugging Nodejs

Setting Up Chrome DevTools for Node.js

Chrome DevTools is the most feature-rich debugger for Node.js. To attach, start your app with --inspect and open chrome://inspect in Chrome. You'll see a list of remote targets. Click 'inspect' to open dedicated DevTools. The Sources tab shows your file system, allows breakpoints, and supports step-through debugging. The Console tab runs in the Node.js context. The Memory and Profiler tabs are invaluable for performance debugging. One common pitfall: if you use --inspect-brk, the app won't start until you attach the debugger. This is useful for debugging startup issues. For production debugging, use --inspect without --inspect-brk to avoid blocking.

debug-chrome.shBASH
1
2
node --inspect-brk app.js
# Then open chrome://inspect and click 'inspect'
Output
Debugger attached.
💡Break on First Line
Use --inspect-brk to pause on the first line of your app. Great for debugging initialization code.
📊 Production Insight
In production, use --inspect with a secure WebSocket connection (wss://) and authentication to prevent unauthorized access.
🎯 Key Takeaway
Chrome DevTools provides a full debugging UI for Node.js via the inspector protocol.

VS Code Integrated Debugger

VS Code's debugger is the most convenient for daily development. It integrates directly with your editor, allowing you to set breakpoints, inspect variables, and watch expressions without leaving your code. To configure, create a .vscode/launch.json file with a Node.js launch configuration. The key configuration is 'type': 'node' and 'request': 'launch' or 'attach'. Use 'attach' to connect to an already running process (e.g., with --inspect). The 'preLaunchTask' option can run a build step before debugging. VS Code also supports conditional breakpoints, logpoints (console.log without code changes), and hit counts. For monorepos, use 'runtimeArgs' to specify the entry point.

.vscode/launch.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/app.js"
    }
  ]
}
💡Skip Internal Files
Add '<node_internals>/**' to skipFiles to avoid stepping into Node.js internal code.
📊 Production Insight
In production, use VS Code's attach mode to debug a running process, but ensure the process is started with --inspect and the port is not exposed publicly.
🎯 Key Takeaway
VS Code's debugger is the best daily driver for Node.js debugging with minimal setup.
debugging-nodejs THECODEFORGE.IO Node.js Debugging Architecture Layered components from application to debugging tools Application Layer Node.js Process | Async Code | Promises Inspector Protocol WebSocket | Chrome DevTools Protocol Debugging Clients Chrome DevTools | VS Code | CLI Inspector Profiling Tools CPU Profiler | Heap Snapshot | Memory Inspector Production Safety Firewall | Authentication | Port Forwarding THECODEFORGE.IO
thecodeforge.io
Debugging Nodejs

Advanced Breakpoints and Stepping

Breakpoints are the core of debugging. Beyond simple line breakpoints, Node.js supports conditional breakpoints (break only when a condition is true), logpoints (log a message without pausing), and hit count breakpoints (break after N hits). In Chrome DevTools, right-click the line number to set a conditional breakpoint. In VS Code, use the breakpoint context menu. Stepping options: Step Over (F10) executes the current line and moves to the next; Step Into (F11) goes into a function call; Step Out (Shift+F11) finishes the current function and returns. Use 'Continue' (F8) to resume execution until the next breakpoint. For async code, be aware that stepping may jump between async contexts.

conditional-breakpoint.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
function processOrder(order) {
  // Set conditional breakpoint: order.total > 1000
  console.log(`Processing order ${order.id}`);
  // ...
}

const orders = [
  { id: 1, total: 500 },
  { id: 2, total: 1500 },
];
orders.forEach(processOrder);
Output
Processing order 1
Processing order 2
Try it live
🔥Logpoints
Logpoints let you log messages without modifying code. Useful for debugging production issues where you can't redeploy.
📊 Production Insight
In production, logpoints are safer than adding console.log because they don't require a redeploy and can be removed instantly.
🎯 Key Takeaway
Master conditional breakpoints and logpoints to debug efficiently without code changes.

Debugging Asynchronous Code and Promises

Async code is where Node.js debugging gets tricky. Callbacks, Promises, and async/await create execution contexts that can be hard to trace. The inspector handles async stacks well: when you break inside an async function, the call stack shows the async context chain. In Chrome DevTools, enable 'Async' in the Call Stack panel to see the full async trace. For Promises, use the 'Promise' tab to inspect pending and settled promises. A common bug: unhandled Promise rejections. Node.js will warn about these, but you can break on them by enabling 'Pause on uncaught exceptions' in the debugger. For async/await, stepping works as expected, but be careful with await inside loops — it can cause performance issues.

async-debug.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function fetchData(url) {
  const response = await fetch(url);
  const data = await response.json();
  return data;
}

async function main() {
  try {
    const data = await fetchData('https://api.example.com/data');
    console.log(data);
  } catch (err) {
    console.error('Failed:', err);
  }
}

main();
Output
{ id: 1, name: 'Example' }
Try it live
⚠ Unhandled Rejections
Always handle Promise rejections. In Node.js 15+, unhandled rejections crash the process. Use process.on('unhandledRejection') to log them.
📊 Production Insight
In production, monitor unhandled rejections with APM tools. They often indicate bugs that surface under load.
🎯 Key Takeaway
Async debugging requires understanding async stack traces and using tools like the Promise tab.

Profiling CPU and Memory

Performance issues often require profiling. Node.js's inspector provides CPU and heap profilers. In Chrome DevTools, the Performance tab records CPU samples and shows a flame graph. The Memory tab can take heap snapshots and compare them to find leaks. To start profiling programmatically, use the built-in inspector module. For CPU profiling, use console.profile() and console.profileEnd(). For heap snapshots, use the v8 module's getHeapSnapshot(). In production, use the --prof flag to generate a V8 log file, then process it with --prof-process. Common issues: memory leaks from closures, large buffers not garbage collected, and CPU spikes from synchronous operations.

profile-cpu.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const inspector = require('inspector');
const fs = require('fs');

function startCPUProfile() {
  const session = new inspector.Session();
  session.connect();
  session.post('Profiler.enable', () => {
    session.post('Profiler.start', () => {
      console.log('CPU profiling started');
    });
  });
  return session;
}

function stopCPUProfile(session) {
  session.post('Profiler.stop', (err, { profile }) => {
    if (!err) {
      fs.writeFileSync('profile.cpuprofile', JSON.stringify(profile));
      console.log('Profile saved');
    }
    session.disconnect();
  });
}

const session = startCPUProfile();
setTimeout(() => stopCPUProfile(session), 5000);
Output
CPU profiling started
Profile saved
Try it live
🔥Heap Snapshots
Take a heap snapshot before and after a suspected leak. Compare them in Chrome DevTools to find retained objects.
📊 Production Insight
In production, run periodic heap snapshots and compare them automatically. A growing heap size over time indicates a leak.
🎯 Key Takeaway
Use CPU and heap profiling to identify performance bottlenecks and memory leaks.

Debugging in Production with --inspect

Debugging in production is risky but sometimes necessary. The safest approach is to use --inspect with a secure WebSocket (wss://) and authentication. Alternatively, use the inspector protocol over a Unix socket to avoid network exposure. Tools like ndb (by Google) provide a secure debugging UI. For containerized apps, expose the inspector port only to a debug sidecar. Never use --inspect without a firewall. A common pattern: start the app with --inspect=0.0.0.0:9229 but bind to localhost in production. Use SSH tunneling to connect: ssh -L 9229:localhost:9229 user@prod-server. Then attach Chrome DevTools to localhost:9229.

ssh-tunnel.shBASH
1
2
3
4
5
6
# On production server
node --inspect=127.0.0.1:9229 app.js &

# On local machine
ssh -L 9229:localhost:9229 user@prod-server
# Then open chrome://inspect and connect to localhost:9229
Output
Debugger listening on ws://127.0.0.1:9229/...
⚠ Security Risk
Exposing the inspector port to the internet is a severe security risk. Always use SSH tunneling or a VPN.
📊 Production Insight
In production, consider using a debug sidecar container that attaches to the main process via Unix socket, keeping the inspector off the network.
🎯 Key Takeaway
Production debugging requires secure tunneling — never expose the inspector port directly.

Sometimes you can't attach a debugger — especially in serverless or ephemeral environments. In those cases, structured logging and distributed tracing are your friends. Use libraries like pino or winston for JSON logging with log levels. Include request IDs, correlation IDs, and timing information. For tracing, use OpenTelemetry to instrument your code and send traces to a backend like Jaeger or Zipkin. This allows you to see the full lifecycle of a request across services. When debugging, add logpoints (via VS Code or Chrome) that log without code changes. In production, use dynamic log levels: change log level at runtime without restarting the app.

structured-logging.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
const pino = require('pino');
const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level(label) {
      return { level: label };
    }
  }
});

logger.info({ requestId: 'abc123', user: 'john' }, 'Processing order');
logger.error({ err: new Error('DB timeout') }, 'Order failed');
Output
{"level":"info","time":1620000000000,"pid":123,"hostname":"server","requestId":"abc123","user":"john","msg":"Processing order"}
{"level":"error","time":1620000001000,"pid":123,"hostname":"server","err":{"message":"DB timeout","stack":"..."},"msg":"Order failed"}
Try it live
💡Dynamic Log Levels
Use a library like pino-dynamic or a custom endpoint to change log levels at runtime. Great for debugging production without restart.
📊 Production Insight
In production, always log with correlation IDs. When a user reports an error, you can trace their entire request across services.
🎯 Key Takeaway
When you can't attach a debugger, structured logging and distributed tracing are essential.

Common Pitfalls and How to Avoid Them

Even with great tools, developers make mistakes. One common pitfall: debugging minified or transpiled code. Always use source maps. In production, ensure your build process generates source maps and uploads them to your error tracking service. Another pitfall: debugging in a cluster or with child processes. The inspector attaches to the main process only. To debug child processes, start them with --inspect and a different port. Use the 'inspect' option in child_process.fork(). Also, beware of breakpoints in hot code paths — they can cause timeouts in production. Use conditional breakpoints sparingly. Finally, don't rely solely on the debugger; combine it with logging and monitoring.

debug-child.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
const { fork } = require('child_process');

const child = fork('worker.js', [], {
  execArgv: ['--inspect=0.0.0.0:9230']
});

child.on('message', (msg) => {
  console.log('From child:', msg);
});
Output
From child: { status: 'ok' }
Try it live
⚠ Source Maps
Always deploy source maps to production. Without them, stack traces point to minified code, making debugging impossible.
📊 Production Insight
In production, use a source map upload step in your CI/CD pipeline. Services like Sentry can ingest them for readable stack traces.
🎯 Key Takeaway
Avoid common pitfalls like missing source maps and debugging child processes incorrectly.

Putting It All Together: A Debugging Workflow

Effective debugging is a workflow. Start with logging: add structured logs at key points. If the issue is reproducible, attach the debugger locally. Use conditional breakpoints to narrow down the problem. For performance issues, profile CPU and memory. For production issues, use SSH tunneling to attach the debugger safely, or rely on logs and tracing. Always have a hypothesis before debugging. Use the scientific method: form a hypothesis, test it with a breakpoint or log, and iterate. Document your findings. After fixing, add a test to prevent regression. Finally, monitor the fix in production with alerts.

debug-workflow.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Step 1: Add structured logging
logger.info({ event: 'order_created', orderId }, 'Order created');

// Step 2: If issue persists, attach debugger
// node --inspect-brk app.js

// Step 3: Set conditional breakpoint
// if (order.total > 1000) debugger;

// Step 4: Profile if needed
// console.profile('order-processing');

// Step 5: Fix and add test
// test('order total > 1000', ...);
Try it live
🔥Scientific Method
Form a hypothesis before setting breakpoints. 'I think the error is in function X because...' Then test it.
📊 Production Insight
In production, automate the workflow: when an alert fires, automatically collect logs, heap snapshots, and CPU profiles for the last 5 minutes.
🎯 Key Takeaway
A systematic workflow — log, debug, profile, fix, test — makes debugging efficient.

CLI Debugger: node inspect Commands, Watchers, and REPL

The built-in CLI debugger is often overlooked but invaluable for quick debugging without an IDE. Start with node inspect app.js. This drops you into a debugger prompt where you can use commands like cont (continue), next, step, out, pause, backtrace, and list(n). Use watch('expression') to monitor variables; watchers lists them. The debugger also provides a REPL: type repl to evaluate expressions in the current scope. For example, watch('req.url') and then repl to inspect req.headers. Exit REPL with Ctrl+D. This is perfect for serverless or CI environments where Chrome DevTools isn't available. The CLI debugger respects the same --inspect protocol, so you can attach later. Pro tip: use node inspect --inspect-brk app.js to break on first line.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ node inspect app.js
< Debugger listening on ws://127.0.0.1:9229/...
< For help, see: https://nodejs.org/en/docs/inspector
< Debugger attached.
break in app.js:1
> 1 const http = require('http');
  2 const hostname = '127.0.0.1';
  3 const port = 3000;
debug> watch('hostname')
debug> watchers
  0: hostname = "127.0.0.1"
debug> cont
< server running at http://127.0.0.1:3000/
... (hit Ctrl+C to stop)
💡CLI Debugger Is Your Fallback
When VS Code or Chrome DevTools aren't available (e.g., in a Docker container without X11), the CLI debugger is your lifeline. Learn the commands.
📊 Production Insight
In production, avoid interactive debugging. Use logging and remote inspection with --inspect only if necessary and secured.
🎯 Key Takeaway
Master the CLI debugger for environments without a GUI. Use watch() and repl to inspect state.

launch.json Advanced Configuration for Node.js

VS Code's launch.json is more powerful than most realize. Beyond basic type: node, you can configure runtimeArgs for --inspect-brk, env for environment variables, cwd for working directory, outputCapture to capture stdout/stderr, and skipFiles to skip node_modules or internal Node.js files. Use preLaunchTask to run a build step before debugging. For TypeScript, set preLaunchTask: "tsc: build - tsconfig.json" and outFiles to map compiled JS. The resolveSourceMapLocations option controls which source maps are resolved. Example: "resolveSourceMapLocations": ["${workspaceFolder}/", "!/node_modules/**"]. You can also use localRoot and remoteRoot for remote debugging. Pro tip: use console: "integratedTerminal" to see console output in the terminal instead of the debug console.

.vscode/launch.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/dist/index.js",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "preLaunchTask": "tsc: build - tsconfig.json",
      "env": { "NODE_ENV": "development" },
      "console": "integratedTerminal",
      "resolveSourceMapLocations": [
        "${workspaceFolder}/**",
        "!**/node_modules/**"
      ]
    }
  ]
}
⚠ Don't Forget skipFiles
Always add "skipFiles": ["<node_internals>/**"] to avoid stepping into Node.js internal code. It saves time.
📊 Production Insight
Never use preLaunchTask that modifies production code. Keep build steps separate.
🎯 Key Takeaway
Customize launch.json with skipFiles, preLaunchTask, and resolveSourceMapLocations for efficient debugging.

Multi-Target Debugging: Debug Multiple Processes Simultaneously

Modern Node.js apps often run multiple processes (e.g., microservices, worker threads, or a frontend + backend). VS Code supports multi-target debugging via a compound configuration. Define each process as a separate launch configuration, then create a compound that runs them together. Example: debug an Express API and a WebSocket server concurrently. Use "stopAll": true to stop all when one stops. For worker threads, use "autoAttachChildProcesses": true in the parent config. This attaches the debugger to any child processes spawned. Alternatively, use "attach" configurations for processes already running with --inspect. Pro tip: set "restart": true to automatically reattach if the process restarts (useful with nodemon).

.vscode/launch.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "API Server",
      "program": "${workspaceFolder}/api/server.js",
      "port": 9229
    },
    {
      "type": "node",
      "request": "launch",
      "name": "WebSocket Server",
      "program": "${workspaceFolder}/ws/server.js",
      "port": 9230
    }
  ],
  "compounds": [
    {
      "name": "Full Stack",
      "configurations": ["API Server", "WebSocket Server"],
      "stopAll": true
    }
  ]
}
💡Use Unique Ports for Each Process
Each --inspect process needs a unique port. Default is 9229, so specify --inspect=9230 for the second.
📊 Production Insight
Multi-target debugging is for development only. In production, use centralized logging and monitoring instead.
🎯 Key Takeaway
Use compound launch configurations to debug multiple Node.js processes simultaneously.

Source Maps for TypeScript: Debug .ts Files Directly

Debugging TypeScript requires source maps to map compiled JavaScript back to TypeScript. Enable "sourceMap": true in tsconfig.json. In VS Code, set outFiles to the output directory and ensure resolveSourceMapLocations includes your source. The debugger will then let you set breakpoints in .ts files. For complex setups (e.g., path mapping, project references), use sourceMapPathOverrides to remap paths. Example: "sourceMapPathOverrides": { "webpack:///./src/": "${workspaceFolder}/src/" } for webpack. Pro tip: always test source maps by setting a breakpoint in a .ts file and verifying it hits. If not, check that the .js file has a //# sourceMappingURL comment.

tsconfig.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "strict": true
  },
  "include": ["src/**/*"]
}
⚠ Source Map Gotcha
If breakpoints in .ts files are not binding, ensure outFiles matches the actual output path and that source maps are not disabled by a bundler.
📊 Production Insight
Source maps should not be deployed to production. Use a build step to strip them or serve them only internally.
🎯 Key Takeaway
Enable sourceMap: true in tsconfig and configure outFiles in launch.json to debug TypeScript directly.

Remote Debugging Over SSH: Debug Node.js on a Remote Server

When your app runs on a remote server (e.g., staging), you can debug it locally using SSH port forwarding. Start the Node.js process with --inspect=0.0.0.0:9229 (bind to all interfaces) but secure it with a firewall. On your local machine, run ssh -L 9229:localhost:9229 user@remote-server. Then in VS Code, create an attach configuration with "port": 9229. Alternatively, use --inspect-publish-uid=http to restrict to specific IPs. For production, never expose the inspector port publicly; use SSH tunneling or a VPN. Pro tip: use --inspect-brk to pause on first line, then attach. This is great for debugging startup issues.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# On remote server
$ node --inspect=0.0.0.0:9229 app.js

# On local machine
$ ssh -L 9229:localhost:9229 user@remote-server

# In VS Code launch.json
{
  "type": "node",
  "request": "attach",
  "name": "Attach to Remote",
  "port": 9229,
  "localRoot": "${workspaceFolder}",
  "remoteRoot": "/path/to/app"
}
💡Never Expose --inspect Publicly
The inspector protocol has no authentication. Always use SSH tunneling or a firewall to restrict access.
📊 Production Insight
Remote debugging in production should be a last resort. Prefer structured logging and APM tools.
🎯 Key Takeaway
Use SSH port forwarding to securely debug Node.js on remote servers with VS Code.
Chrome DevTools vs VS Code Debugger Trade-offs for Node.js debugging environments Chrome DevTools VS Code Setup Complexity Manual attach via URL One-click launch config Breakpoint Types Line, conditional, logpoint Line, conditional, function Async Debugging Async stack traces limited Full async/await support Profiling Integration Built-in CPU/memory profiler Requires extension or CLI Production Use Direct --inspect connection Via attach config with port THECODEFORGE.IO
thecodeforge.io
Debugging Nodejs

Heap Snapshot Generation and Memory Leak Debugging

Memory leaks in Node.js can be elusive. Generate heap snapshots using the v8 module: require('v8').getHeapSnapshot().pipe(fs.createWriteStream('heap.heapsnapshot')). Load the snapshot in Chrome DevTools (Memory tab) to compare objects. For automated leak detection, use heapdump package: heapdump.writeSnapshot('/tmp/heap-1.heapsnapshot'). Compare two snapshots to see retained objects. Clinic.js (specifically clinic heapprofiler) provides a flamegraph-like view. Pro tip: trigger snapshots on demand via a signal: process.on('SIGUSR2', () => heapdump.writeSnapshot()). For long-running processes, use --max-old-space-size to set memory limits and --trace-gc to log GC events.

snapshot.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const v8 = require('v8');
const fs = require('fs');

// Generate heap snapshot
const snapshotStream = v8.getHeapSnapshot();
const fileStream = fs.createWriteStream('/tmp/heap.heapsnapshot');
snapshotStream.pipe(fileStream);

// Or using heapdump package
const heapdump = require('heapdump');
heapdump.writeSnapshot('/tmp/heap-' + Date.now() + '.heapsnapshot', (err, filename) => {
  if (err) console.error(err);
  else console.log('Snapshot written to', filename);
});
Try it live
💡Compare Snapshots
Take two snapshots (before and after a suspected leak), then use Chrome DevTools' Comparison view to find retained objects.
📊 Production Insight
Automate heap snapshot generation on memory threshold alerts, but never block the event loop. Use a separate process.
🎯 Key Takeaway
Use v8.getHeapSnapshot() or heapdump to capture heap snapshots and analyze memory leaks.
● Production incidentPOST-MORTEMseverity: high

The Silent Memory Leak: How a Forgotten setInterval Brought Down Our API Gateway

Symptom
API gateway process memory grew linearly from 200MB to 2GB over 48 hours, then OOM-killed by the OS. No obvious error logs, just a sudden restart.
Assumption
We assumed a third-party SDK was leaking, as the issue appeared after an update. We spent days profiling the SDK's heap usage.
Root cause
A health-check endpoint used setInterval to cache external service status, but never called clearInterval when the server shut down or the route was called again. Each request created a new interval, each holding a reference to the request object and a large closure.
Fix
Replaced the interval with a single shared timer using a module-level variable. Added clearInterval on server shutdown (process.on('SIGTERM')). Also added a memory usage alert in our monitoring.
Key lesson
  • Always clear timers when they are no longer needed, especially in request handlers.
  • Use process.on('SIGTERM') to cleanly shut down resources.
  • Monitor memory growth trends, not just absolute values.
  • Profile heap snapshots regularly in staging to catch leaks early.
⚙ Quick Reference
14 commands from this guide
FileCommand / CodePurpose
start-inspector.shnode --inspect app.jsWhy Node.js Debugging Is Different
debug-chrome.shnode --inspect-brk app.jsSetting Up Chrome DevTools for Node.js
.vscodelaunch.json{VS Code Integrated Debugger
conditional-breakpoint.jsfunction processOrder(order) {Advanced Breakpoints and Stepping
async-debug.jsasync function fetchData(url) {Debugging Asynchronous Code and Promises
profile-cpu.jsconst inspector = require('inspector');Profiling CPU and Memory
ssh-tunnel.shnode --inspect=127.0.0.1:9229 app.js &Debugging in Production with --inspect
structured-logging.jsconst pino = require('pino');javascript configuration
debug-child.jsconst { fork } = require('child_process');Common Pitfalls and How to Avoid Them
debug-workflow.jslogger.info({ event: 'order_created', orderId }, 'Order created');Putting It All Together
$ node inspect app.jsCLI Debugger
tsconfig.json{Source Maps for TypeScript
$ node --inspect=0.0.0.0:9229 app.jsRemote Debugging Over SSH
snapshot.jsconst v8 = require('v8');Heap Snapshot Generation and Memory Leak Debugging

Key takeaways

1
Inspector Protocol
Node.js debugging relies on the inspector protocol; tools like Chrome DevTools and VS Code use it under the hood.
2
Async Debugging
Async code requires understanding async stack traces; use the Promise tab and enable async call stacks.
3
Production Safety
Never expose the inspector port directly; use SSH tunneling or Unix sockets for production debugging.
4
Systematic Workflow
Combine logging, breakpoints, profiling, and testing into a repeatable debugging process.
5
CLI Debugger
The node inspect command provides a full debugger with watchers and REPL, essential for headless environments.
6
launch.json Advanced Config
Customize skipFiles, preLaunchTask, and resolveSourceMapLocations to streamline TypeScript and complex project debugging.
7
Heap Snapshots
Use v8.getHeapSnapshot() or heapdump to capture memory snapshots; compare them in Chrome DevTools to find leaks.
8
CLI Debugger
Use node inspect with watchers and REPL for debugging in headless environments.
9
Remote Debugging
SSH port forwarding with --inspect allows secure remote debugging from VS Code.
10
Memory Leak Debugging
Generate heap snapshots with v8.writeHeapSnapshot() and analyze with Chrome DevTools or Clinic.js.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you start the Node.js Inspector and connect Chrome DevTools?
Q02JUNIOR
What is the difference between `--inspect` and `--inspect-brk`?
Q03SENIOR
How do you debug a Node.js application in VS Code?
Q04SENIOR
Explain how to use the Chrome DevTools memory profiler to find a memory ...
Q05SENIOR
How would you debug a production Node.js process without stopping it?
Q06SENIOR
Describe a scenario where you'd use async stack traces in Node.js debugg...
Q01 of 06JUNIOR

How do you start the Node.js Inspector and connect Chrome DevTools?

ANSWER
Run node --inspect app.js to start the inspector on a default port (9229). Then open chrome://inspect in Chrome, click 'Open dedicated DevTools for Node', and you'll see your app's console, sources, and profiler.
FAQ · 11 QUESTIONS

Frequently Asked Questions

01
How do I start Node.js with the debugger?
02
Can I debug a running Node.js process?
03
How do I debug async/await code?
04
Is it safe to debug in production?
05
How do I find memory leaks?
06
What's the difference between --inspect and --inspect-brk?
07
What is the difference between --inspect and --inspect-brk?
08
How can I debug memory leaks in a production Node.js app without downtime?
09
Can I use ndb or llrt instead of the built-in inspector?
10
How do I debug memory leaks in a production Node.js app without stopping it?
11
Can I use ndb or llrt instead of the built-in debugger?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

6 min read · try the examples if you haven't

Previous
CommonJS vs ES Modules in Node.js — A Complete Guide
21 / 47 · Node.js
Next
Environment Variables in Node.js with dotenv