Menu
CoddyTech

Node.js Performance and Scaling Interview Questions

Using every CPU core and keeping CPU-heavy work off the event loop.

10 questions
  1. What is the cluster module and how does it work?

    cluster forks copies of the server, usually one per core, sharing one port; the primary hands connections to workers round-robin (except on Windows).

    const cluster = require('node:cluster');
    const http = require('node:http');
    const os = require('node:os');
    
    if (cluster.isPrimary) {
      for (let i = 0; i < os.availableParallelism(); i++) cluster.fork();
      cluster.on('exit', (worker, code) => {
        console.log(`worker ${worker.process.pid} exited with ${code}, starting a new one`);
        cluster.fork();
      });
    } else {
      http.createServer((req, res) => res.end(`handled by ${process.pid}\n`)).listen(3000);
    }
    

    Workers share no memory, so sessions and caches move to Redis. In containers the usual choice is one process per container instead.

  2. What are worker threads in Node.js, and when do you use them?

    worker_threads runs JavaScript in parallel threads in one process, each with its own V8 isolate and event loop, talking by messages or a SharedArrayBuffer. Use them for CPU-heavy work.

    const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
    
    function fib(n) {
      return n < 2 ? n : fib(n - 1) + fib(n - 2);
    }
    
    if (isMainThread) {
      const worker = new Worker(__filename, { workerData: 35 });
      worker.on('message', (result) => console.log('fib(35) =', result));
      worker.on('error', (err) => console.error(err));
      console.log('main thread is still free');
    } else {
      parentPort.postMessage(fib(workerData));
    }
    // Node.js prints:
    // main thread is still free
    // fib(35) = 9227465
    

    Not for I/O, which is already async. Keep a fixed pool (piscina) rather than one worker per request.

  3. What is the difference between cluster, worker_threads and child_process?

    clusterworker_threadschild_process
    Unitprocessthreadprocess
    Memoryseparateseparate heaps, optional SharedArrayBufferseparate
    Use forall cores for HTTPCPU-heavy JavaScriptother programs (ffmpeg, git)

    A thread is cheaper than a process, but a native crash in a worker kills the whole process, while a crashed child leaves the parent running.

  4. What is the difference between spawn, exec, execFile and fork?

    FunctionShellOutputUse for
    spawnno (by default)streamslong processes, large output
    execyesbuffered (1 MB default)short shell commands
    execFilenobuffereda program with arguments
    forknostdio plus IPCa Node.js script you message

    exec('convert ' + userFile) is command injection. Pass user input as an argument array to execFile or spawn.

  5. How do you handle a CPU-intensive task in a Node.js API?

    Get it off the event loop: a job queue (BullMQ, SQS) when the client can wait, replying 202 with a job id; a worker thread pool when the response needs the result; or chunking with setImmediate as a last resort. The wrong answer is async: wrapping a synchronous loop in an async function does not move it to another thread.

  6. How do you find out why a Node.js service is slow?

    Measure, then tell the kinds of slow apart:

    • Event loop blocked (every endpoint slow, one core at 100%): CPU profile with node --cpu-prof or --inspect, read the flame graph.
    • Waiting on I/O (CPU low): time database and downstream calls, check pool limits, including the libuv pool.
    • Memory pressure (RSS rising, latency spikes): GC pauses; use heap snapshots.

    perf_hooks.monitorEventLoopDelay() exported as a metric shows a blocked loop.

  7. What is the V8 heap limit in Node.js and how do you change it?

    V8 caps the old-generation heap, and past it the process dies with "JavaScript heap out of memory". Since Node 12 the default depends on available memory; read it from v8.getHeapStatistics().heap_size_limit and change it with --max-old-space-size=4096 (MiB), below the container limit. If usage grows without end it is a leak, and a bigger heap only delays the crash.

  8. How would you add caching to a Node.js API?

    Pick by scope: an in-process LRU Map for small hot data (each process has its own copy), Redis for anything shared across instances, and HTTP caching (Cache-Control, a CDN) for public GET responses. Two traps: an unbounded Map cache is a memory leak, so cap it and expire entries; and when a hot key expires, many requests recompute it at once, so cache the in-flight promise.

  9. What is PM2, and why run Node.js under a process manager?

    PM2 is a process manager for Node.js: it restarts the app when it crashes, runs one instance per core in cluster mode, reloads workers one by one for zero-downtime deploys, collects logs and starts the app again after a reboot.

    pm2 start server.js -i max --name api   # one process per core
    pm2 reload api                          # restart workers one at a time
    pm2 logs api
    pm2 startup && pm2 save                 # start again after a reboot
    

    In Docker or Kubernetes the orchestrator already restarts and scales containers, so run plain node, one process per container, instead of PM2.

  10. Why use a database connection pool in Node.js, and how big should it be?

    Opening a connection costs a TCP, TLS and login round trip, and one connection runs one query at a time. A pool keeps a few connections open and lends one per query.

    const { Pool } = require('pg');
    const pool = new Pool({ max: 10 });
    
    const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
    
    const client = await pool.connect(); // a transaction needs one connection
    try {
      await client.query('BEGIN');
      await client.query('UPDATE accounts SET balance = balance - 100 WHERE id = $1', [from]);
      await client.query('COMMIT');
    } catch (err) {
      await client.query('ROLLBACK');
      throw err;
    } finally {
      client.release(); // forget this and the pool runs dry
    }
    

    Size it small: pool size times instances must stay under the database's limit (PostgreSQL's max_connections defaults to 100), and more connections than the database has cores rarely helps.

Coddy programming languages illustration

Learn to code with Coddy

Get started