Home / Articles / Node.js API Performance: A Priority-Ordered Optimization Framework

This article is published in English.

Node.js API Performance: A Priority-Ordered Optimization Framework

Learn how to triage Node.js API performance fixes into effort-versus-impact tiers so you fix connection pooling and N+1 queries before chasing exotic optimizations.

1300 words

Most write-ups on speeding up a Node.js API throw a flat list of fifteen tips at you, with something like a faster JSON serializer sitting next to database connection pooling, as though each item deserves the same slice of your attention. That's misleading. Some fixes take ten minutes and cut your response times dramatically. Others take months of effort for a marginal gain. Understanding the difference matters far more than memorizing every item on the list.

Tier 1: Do These First (High Impact, Low Effort)

These changes cost almost nothing to implement. They require little code, carry little risk, and in practice they're the actual source of slowness far more often than the exotic fixes people reach for instead.

Turn on keep-alive for any outbound HTTP requests. By default, Node's HTTP client opens a new connection for every call, so each request to an external service pays the full price of a TCP handshake plus TLS negotiation all over again. Reusing a single agent across requests eliminates that overhead for every subsequent call to the same host.

const agent = new https.Agent({ keepAlive: true, maxSockets: 50 });

Always talk to your database through a connection pool, not a lone connection. Under real concurrent load, a single connection effectively becomes a queue that everything backs up behind. A pool sized appropriately lets your API handle concurrent traffic the way it's genuinely structured, in parallel.

Let independent asynchronous calls run in parallel instead of one after another. When two await statements don't rely on each other's output, there's no reason to force them to wait in line.

// costs the sum of both calls
const user = await getUser(id);
const orders = await getOrders(id);

// costs roughly the slower of the two
const [user, orders] = await Promise.all([getUser(id), getOrders(id)]);

Add indexes for the columns your queries actually filter, join, or sort on. Of everything on this list, this is arguably the highest-leverage move for any endpoint reading from a table that keeps growing, and it's frequently a one-line migration to add.

Eliminate N+1 query patterns. Fetching a list and then issuing another query per item inside a loop looks harmless with a handful of test records and becomes a serious problem once you're dealing with thousands of real ones. Replace the loop with a single batched query for the related data.

Cap every query that could otherwise return an unbounded result set with a LIMIT. An endpoint that returns "every order this customer has ever placed" with no ceiling performs fine for a brand-new account and falls apart for one with years of order history behind it.

Tier 2: Worth Real Investment (High Impact, Real Effort)

The items in this tier aren't small tweaks. They call for actual design work, and often new infrastructure, but they solve categories of problems that no Tier 1 fix can touch.

Introduce a caching layer for reads that are expensive and happen often. Putting Redis in front of a slow aggregate query or a costly call to a third-party API can bring a 200ms response down to roughly 2ms. The hard part isn't standing up the cache, it's designing an invalidation strategy solid enough that the cache never quietly hands out stale results.

Take slow, non-critical work off the request-response cycle entirely. Sending a confirmation email, building a report, updating analytics data, none of it has to finish before you reply to the client. Pairing a queue, such as BullMQ or SQS, with a dedicated worker process can shrink a request that used to take 2 seconds down to something closer to 80 milliseconds.

Move from offset-based pagination to cursor-based pagination for large or deep result sets. Pagination built on OFFSET gets progressively slower as pages go deeper, because the database still has to scan past every row that came before. A cursor-based approach costs roughly the same whether you're on page 5 or page 5,000.

Scale out horizontally with a real load balancer and centralized session or cache state. No matter how well you've tuned it, a single Node process eventually hits a ceiling. Running several instances behind a load balancer, each sharing a common Redis cache and connection pool, pushes that ceiling higher without demanding that any one process get individually faster.

Profile before you optimize any further. Once the obvious problems are fixed, guessing at what's slow stops being a reliable strategy. Using an actual profiler or APM tool, something like clinic.js, a hosted APM product, or even running EXPLAIN ANALYZE against a suspect query, shows you where time is genuinely being spent instead of where you assume it must be.

Tier 3: Usually Not Worth Prioritizing (Low Impact, Often Overrated)

These come up constantly in performance discussions, yet they rarely make a meaningful difference on a real production API, mainly because they target parts of the stack that were never the actual bottleneck to begin with.

Fine-tuning JSON serialization. Faster JSON libraries do exist and do help, but only at a scale the vast majority of APIs never approach. If a 300ms database query is your real problem, trimming a few milliseconds off serialization is solving the wrong issue.

Swapping frameworks purely to shave off framework overhead. The performance gap between Express and a supposedly faster alternative is real but small compared to what an unindexed table or an N+1 query is costing you. Framework choice can matter for plenty of other reasons; raw speed usually isn't one of them.

Reaching for clustering before addressing everything else first. Spinning up multiple Node processes to make use of extra CPU cores helps genuinely CPU-bound work. It won't do anything for an endpoint that's slow because it's stuck waiting on an unindexed query, since waiting is waiting no matter how many processes are sitting idle doing it.

Rewriting hot code paths in a lower-level language purely for performance. There are legitimate cases for this, when a specific, proven, CPU-bound bottleneck justifies it. Far more often, though, it gets attempted before anyone has actually confirmed that's where the time is going, turning a real technique into wasted effort aimed at the wrong target.

How to Actually Use This

Clear out all of Tier 1 before you consider anything else, since it's inexpensive, low-risk, and covers the majority of real-world performance problems. Only move on to Tier 2 for the specific endpoints where profiling shows Tier 1 wasn't sufficient, rather than treating it as a rewrite you apply everywhere. Leave Tier 3 untouched until you have concrete evidence, from a profiler rather than a guess, that one of those specific techniques is the actual bottleneck. Most APIs that feel sluggish are slow because of an unaddressed Tier 1 issue sitting there, not because they're missing some obscure optimization pulled from the bottom of a blog post.