StackInterview logoStackInterview icon

Explore

Interview Qn

Interview AI

Problems

Resume Builder

Quiz

Articles

DevKit

StackInterview

The complete platform to prepare for full-stack developer interviews - questions, AI mock interviews, coding practice, and guides.

Free to start

Platform

  • Interview Questions
  • Interview AI
  • Coding Problems
  • Company Tracks
  • Stack Quiz
  • JS Playground
  • Resume Builder

Guides

  • 50 Playwright Interview Questions 2026
  • Node.js Performance Tips: From 100 to 10,000 Requests Per Second (+ Interview Questions)
  • The New HTTP QUERY Method Explained (RFC 10008) + 10 Interview Questions
  • Top 25 Node.js Interview Questions Every Developer Should Know
  • How to Decode, Validate, and Generate JWTs Online for Free (2026 Guide)
  • View all guides →

Company

  • About Us
  • Guides & Articles
  • FAQ
  • Contact
  • Pricing
  • Privacy Policy
  • Terms of Service

© 2026 StackInterview. Built for engineers, by engineers.

PrivacyTermsContact
All Articles
📁Interview Prep13 min read

Node.js Performance Tips: From 100 to 10,000 Requests Per Second (+ Interview Questions)

Five practical Node.js performance fixes - compression, clustering, connection pooling, memory leak hunting, and N+1 queries - paired with the interview questions each one triggers.

Most Node.js apps aren't slow because the runtime is slow. They're slow because of five fixable things: no compression, one CPU core in use, no connection pooling, leaking memory, and N+1 queries. Here's how to fix each one, and the interview questions that come up once you do.

nodejsperformanceinterview-questionsbackendscalingnodejs-2026
On this page
  1. Node.js Performance Tips: From 100 to 10,000 Requests Per Second (+ Interview Questions)
  2. Why Do Most Node.js Apps Plateau Before They Hit Real Load?
  3. How Much Does Compression Actually Improve Throughput?
  4. How Does the Cluster Module Turn One Core Into All of Them?
  5. Why Does Opening a New Database Connection Per Request Kill Throughput?
  6. What's Actually Causing Your Node.js Memory Leak?
  7. Why Is a Query-in-a-Loop the Most Common Database Bug in Interviews?
  8. How Do You Know Which Fix to Apply First?
  9. What Other Node.js Performance Questions Come Up in Interviews?
  10. Frequently Asked Questions
  11. Which of these five fixes gives the biggest win for the least effort?
  12. Do I need all five fixes, or can I stop after the quick wins?
  13. Will these questions come up in a junior Node.js interview, or only senior rounds?
  14. Is `autocannon` the only benchmarking tool worth knowing?
  15. Conclusion
Summarize with AI
ChatGPT
ChatGPT
Perplexity
Perplexity
Claude
Claude
Google AI
Google AI
Grok
Grok
Practice

Test your knowledge

Real interview questions asked at top product companies.

Practice Now
More Articles
Summarize with AI
ChatGPT
ChatGPT
Perplexity
Perplexity
Claude
Claude
Google AI
Google AI
Grok
Grok

Most Node.js apps aren't slow because Node.js is slow. They're slow because of a handful of unglamorous gaps: no response compression, one CPU core doing all the work, a fresh database connection opened per request, a memory leak nobody's chased down, or a query loop that should've been a single join. Fix those five, and a server handling 100 requests a second can realistically handle 10x that.

For the runtime fundamentals behind these fixes - the event loop, libuv, and the thread pool - see the complete Node.js interview questions guide.

Abstract dark technology visual representing Node.js performance and backend infrastructure
Abstract dark technology visual representing Node.js performance and backend infrastructure

Why Do Most Node.js Apps Plateau Before They Hit Real Load?

Node.js apps usually stall for structural reasons, not because the language is inherently slow: uncompressed responses, a single CPU core in use, and connections opened fresh per request all cap throughput long before the JavaScript itself becomes the bottleneck. The fixes below target those structural limits directly, in the order that gives you the most throughput per line of code changed.

The gap between "100 req/sec" and "10,000 req/sec" is almost never one big rewrite. It's five small, independent changes that compound - compression cuts payload size, clustering multiplies available compute, and pooling removes per-request connection overhead. None of them require touching your business logic.


How Much Does Compression Actually Improve Throughput?

Gzip compression shrinks response payloads before they hit the network, which cuts transfer time and lets your server serve more requests in the same window - a change small enough to add in one line of middleware. It costs a small amount of CPU per request to compress, but for JSON-heavy APIs that CPU cost is almost always cheaper than the bandwidth and latency it saves.

const express = require("express");
const compression = require("compression");

const app = express();

// Apply before your routes - compresses every response that qualifies
app.use(compression());

app.get("/api/products", async (req, res) => {
  const products = await db.products.findAll();
  res.json(products); // now gzipped automatically
});

Interview questions on compression:

  • "When would you skip compression for a route?" Skip it for already-compressed payloads (images, videos, zipped files) and for tiny responses - the compression overhead can exceed the bytes saved. compression() has a threshold option (default 1KB) for exactly this reason.
  • "Does compression happen on the event loop?" Yes - zlib's synchronous compression runs on the main thread by default for small payloads, though Node's zlib module can offload larger jobs. This is why compression is cheap per-request but not free at extreme request volume.
  • "How would you verify compression is actually working?" Check the Content-Encoding: gzip response header, or compare payload size with curl -H "Accept-Encoding: gzip" -o /dev/null -w "%{size_download}\n" <url> against an uncompressed request.

How Does the Cluster Module Turn One Core Into All of Them?

Node.js runs your JavaScript on a single thread, so a server with 16 cores uses exactly one of them by default - the cluster module forks a worker process per CPU core, and the OS load-balances incoming connections across all of them automatically. This is the single highest-leverage fix on this list: it can multiply request-handling capacity by the number of cores on the box, with no application code changes.

const cluster = require("node:cluster");
const os = require("node:os");
const http = require("node:http");

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  for (let i = 0; i < numCPUs; i++) cluster.fork();

  cluster.on("exit", (worker) => {
    console.log(`Worker ${worker.process.pid} died - restarting`);
    cluster.fork(); // keep the pool at full strength
  });
} else {
  http.createServer((req, res) => {
    res.end(`Handled by worker ${process.pid}`);
  }).listen(3000);
}

Interview questions on clustering:

  • "Do cluster workers share memory?" No - each worker is a separate OS process with its own heap and event loop. Anything that needs to be shared across workers (sessions, rate-limit counters, caches) has to live externally, typically in Redis.
  • "When would you reach for worker threads instead of cluster?" Cluster scales concurrent I/O-bound requests across cores. Worker threads solve a different problem - offloading a single CPU-heavy task (image processing, hashing) off the main thread without blocking it, and they can share memory via SharedArrayBuffer.
  • "How do you run cluster mode in production without hand-rolling it?" Most teams reach for a process manager instead: pm2 start server.js -i max clusters across all cores and handles restarts, without you writing the fork logic yourself.

Why Does Opening a New Database Connection Per Request Kill Throughput?

Every new database connection means a fresh TCP handshake and an authentication round-trip - typically 10-100ms of pure overhead before your query even runs, and thousands of concurrent Node.js requests will exhaust the database's connection limit fast if each one opens its own. A connection pool keeps a fixed set of connections open and hands them out on demand, reusing what's already established.

const { Pool } = require("pg");

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // cap concurrent connections - don't over-pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

async function getUser(id) {
  // borrows a connection, runs the query, returns it to the pool
  const { rows } = await pool.query("SELECT * FROM users WHERE id = $1", [id]);
  return rows[0];
}

Interview questions on connection pooling:

  • "How do you pick the max pool size?" It's bounded by the database's connection limit divided across every app instance, not by how much load you're handling - a pool of 20 per instance times 10 clustered workers is already 200 connections. Start conservative and watch for pool-exhaustion errors under load.
  • "What happens when every connection in the pool is busy?" The next pool.query() call queues until a connection frees up or connectionTimeoutMillis is hit, at which point it rejects. That's a deliberate backpressure valve, not a bug.
  • "Is connection pooling only relevant to SQL databases?" No - MongoDB drivers, Redis clients, and HTTP keep-alive agents all pool connections for the same reason: avoiding repeated handshake cost per request.

What's Actually Causing Your Node.js Memory Leak?

PERSONAL EXPERIENCE - The most common Node.js memory leaks in production trace back to three patterns: event listeners registered per-request and never removed, unbounded in-memory caches (a plain object or Map with no eviction), and closures that accidentally hold a reference to a large object long after it's needed.

A Node.js memory leak happens when something still holds a reference to an object the application no longer needs, so the garbage collector can never reclaim it - and rss in process.memoryUsage() climbing steadily under steady load is the clearest early signal. Chrome DevTools, attached via node --inspect, lets you take heap snapshots and diff them to see exactly what's accumulating.

// Leak: listener piles up on every request, never removed
app.get("/dashboard", (req, res) => {
  metricsEmitter.on("update", updateDashboard); // never cleaned up
  res.render("dashboard");
});

// Fix: register once outside the handler, or use a bounded cache
const cache = new Map();
function setCached(key, value, maxSize = 1000) {
  if (cache.size >= maxSize) {
    cache.delete(cache.keys().next().value); // evict oldest
  }
  cache.set(key, value);
}

Interview questions on memory leaks:

  • "How would you confirm a memory leak in production without SSH-ing into the box?" Expose process.memoryUsage().rss on a metrics endpoint or dashboard and watch the trend over hours, not seconds - a leak shows up as a line that never comes back down after garbage collection, not as a spike.
  • "What's the difference between a memory leak and normal memory growth?" Normal growth plateaus as the garbage collector reclaims unused objects between requests. A leak keeps climbing indefinitely because something external to the GC - a live reference - is holding objects hostage.
  • "Name three code patterns that commonly cause leaks." Event listeners added per-request without removal, unbounded caches or arrays that grow forever, and timers (setInterval) that are never cleared when the thing they're tracking goes away.

Why Is a Query-in-a-Loop the Most Common Database Bug in Interviews?

An N+1 query problem happens when code fetches a list, then loops over it firing one additional query per item - turning what should be one or two database round-trips into hundreds, and it's often invisible in local testing because a dev seed dataset is too small to expose it. Replacing the loop with a single join, or an IN (...) batch query, collapses N+1 calls into one.

// N+1: one query for orders, then one query PER order for its items
const orders = await db.query("SELECT * FROM orders WHERE user_id = $1", [userId]);
for (const order of orders) {
  order.items = await db.query("SELECT * FROM items WHERE order_id = $1", [order.id]);
}

// Fixed: single join, grouped in application code
const rows = await db.query(
  `SELECT o.*, i.* FROM orders o
   JOIN items i ON i.order_id = o.id
   WHERE o.user_id = $1`,
  [userId]
);

Interview questions on N+1 queries:

  • "How do you spot an N+1 query in a code review?" Look for a database call sitting inside a .map(), .forEach(), or for loop that iterates over a previous query's results - that shape is the tell, regardless of the ORM.
  • "Is a join always the right fix?" Usually, but not always - if you're using an ORM, batching with a DataLoader-style pattern (collect IDs, fire one IN (...) query, map results back) avoids a join when the relationship is more complex than SQL handles cleanly.
  • "Would an ORM like Prisma or Sequelize prevent this automatically?" No - most ORMs will happily generate N+1 queries unless you explicitly ask for eager loading (include in Prisma, .populate() in Mongoose). The ORM doesn't know your access pattern; you have to tell it.

How Do You Know Which Fix to Apply First?

Guessing the bottleneck wastes more time than measuring it - load-test the endpoint with a tool like autocannon before touching any code, then re-run the same benchmark after each change to confirm it actually moved the number. Fixing compression when the real bottleneck is a database connection ceiling won't show up as a win, and it'll look like the tip didn't work.

npx autocannon -c 100 -d 20 http://localhost:3000/api/products
# -c 100: 100 concurrent connections
# -d 20:  run for 20 seconds
# Look at requests/sec and latency percentiles (p50, p99) in the output

Interview questions on benchmarking:

  • "Why measure before optimizing instead of just applying known best practices?" Because the "known best practice" might not be your bottleneck - a CPU-bound synchronous loop won't be fixed by connection pooling, and applying it anyway burns time without moving the metric that matters.
  • "What's the difference between throughput and latency, and why do you need both?" Throughput (req/sec) tells you total capacity; latency percentiles (p50, p99) tell you the experience of an individual request. A change can raise throughput while making tail latency worse for unlucky requests - you need both numbers to know if a fix actually helped.
  • "What would you check if autocannon shows high latency but low CPU usage?" That pattern usually points away from compute and toward I/O wait - an unpooled database connection, a slow downstream API call, or event loop lag from a blocking operation. Check perf_hooks' monitorEventLoopDelay next.

What Other Node.js Performance Questions Come Up in Interviews?

Beyond the five fixes above, interviewers regularly probe adjacent performance topics - caching layers, event loop health, and HTTP-level tuning - to see if your understanding generalizes past the specific tips you memorized.

Caching

  • "Where would you add a cache, and what would you use?" In front of expensive, frequently-read, rarely-changing data - an in-process LRU cache (lru-cache) for ultra-hot single-instance reads, and Redis for anything that needs to be shared across clustered workers or multiple app instances.
  • "How do you avoid serving stale data from a cache?" Set a TTL matched to how often the underlying data actually changes, and invalidate explicitly on writes rather than relying on TTL alone for anything user-facing (e.g., delete the cache key right after an update, don't wait for expiry).

Event loop health

  • "How do you detect event loop lag before users notice it?" perf_hooks' monitorEventLoopDelay() tracks how long the loop is delayed between iterations; alert when the mean delay crosses a threshold like 50-100ms, since that's roughly where users start perceiving slowness.
  • "What's a common cause of event loop blocking that isn't an obvious infinite loop?" Synchronous JSON parsing or stringifying of a large payload, synchronous crypto (crypto.pbkdf2Sync), and array methods run over very large in-memory datasets - all of these look like normal code but hold the thread for milliseconds to seconds.

HTTP and networking

  • "What does HTTP keep-alive do for throughput?" It reuses an existing TCP connection across multiple requests instead of renegotiating a handshake each time - Node's default HTTP agent enables this, but outbound calls to other services need keepAlive: true set explicitly on the agent to benefit.
  • "How does HTTP/2 change performance characteristics versus HTTP/1.1?" HTTP/2 multiplexes many requests over a single connection, removing the six-connections-per-origin limit browsers impose on HTTP/1.1 - which matters most for APIs serving many small resources to the same client.

Process and deployment

  • "How do you deploy a performance fix without dropping in-flight requests?" Pair clustering with graceful shutdown - stop accepting new connections on SIGTERM, let in-flight requests finish, then exit - and roll workers one at a time (pm2 reload) instead of restarting all of them simultaneously.
  • "If req/sec looks fine locally but drops under production load, what do you check first?" Whatever differs between the two environments: connection pool size against the real database's connection limit, whether clustering is actually enabled in the production process manager config, and network latency to a database or cache that's co-located in dev but remote in production.

Frequently Asked Questions

Which of these five fixes gives the biggest win for the least effort?

Clustering usually wins - forking one worker per CPU core can multiply request-handling capacity by the number of cores with zero changes to business logic. Compression is a close second: it's a one-line middleware add that commonly cuts response transfer time enough to noticeably raise throughput on JSON-heavy APIs.

Do I need all five fixes, or can I stop after the quick wins?

Compression, clustering, and connection pooling cover most apps that plateau under moderate load. Memory leaks and N+1 queries only need chasing once you're seeing symptoms - climbing RSS over hours, or query counts that don't match request counts - so profile before assuming you need every fix.

Will these questions come up in a junior Node.js interview, or only senior rounds?

Compression, clustering basics, and N+1 queries show up at junior and mid-level - they're common enough that most working backend engineers hit them. Memory leak debugging and benchmark-driven optimization skew senior, since they assume you've owned a service in production long enough to have chased a real leak.

Is `autocannon` the only benchmarking tool worth knowing?

It's the fastest to reach for locally, but clinic.js (clinic doctor, clinic flame) goes further by profiling why a request is slow - event loop lag, I/O wait, or CPU-bound code - rather than just measuring req/sec. Know both: autocannon to measure, clinic to diagnose.


Conclusion

None of these five fixes touch business logic, and that's the point - compression, clustering, and connection pooling are configuration-level changes, while memory leaks and N+1 queries are pattern-recognition problems you fix once you know what to look for. Benchmark first, apply the fix that matches the bottleneck you actually measured, then benchmark again to confirm it worked.

For the deeper runtime questions these tips lead into - the event loop, worker threads, and production troubleshooting - see the Node.js interview questions guide and the advanced Node.js interview guide for staff-level scaling and security questions.

More from Interview Prep

💻

The New HTTP QUERY Method Explained (RFC 10008) + 10 Interview Questions

10 min read

📁

Top 25 Node.js Interview Questions Every Developer Should Know

16 min read

🎯

Advanced Playwright Interview Questions and Answers (2026)

17 min read

Browse All Articles