Optimizing NestJS Performance with HTTP Keep-Alive
HTTP Keep-Alive, also known as HTTP persistent connection, is the practice of reusing a single TCP connection to send multiple HTTP requests and responses,…
HTTP Keep-Alive, also known as HTTP persistent connection, is the practice of reusing a single TCP connection to send multiple HTTP requests and responses, instead of opening a new connection for every request (HTTP persistent connection – Wikipedia). In modern HTTP/1.1, connections are persistent by default, meaning the server won’t close the TCP socket immediately after responding (unless explicitly told to do so). Enabling and tuning Keep-Alive can have a big impact on performance for high-throughput APIs and microservices. This post will explain what Keep-Alive is, why it matters for performance, and how to use it effectively in a NestJS application – both on the server side and when your NestJS app makes HTTP requests to others. We’ll include code examples and discuss best practices (like timeouts, max sockets, and connection reuse) as well as when Keep-Alive is beneficial and when it might be counterproductive. What is HTTP Keep-Alive and Why Does It Matter? HTTP Keep-Alive (persistent connections) means reusing the same TCP connection for multiple HTTP requests/responses instead of closing it after each response (HTTP persistent connection – Wikipedia). By default, a naive HTTP/1.1 client-server exchange would go through a cycle of: open TCP connection -> send request -> get response -> close connection. With Keep-Alive, the “close connection” step is deferred, allowing subsequent requests to skip the TCP handshake overhead and use the existing channel. This reuse of connections has several performance benefits: Reduced Latency: Subsequent requests have no TCP handshake or TLS negotiation delay, avoiding extra round-trips (HTTP persistent connection – Wikipedia). For example, a TLS handshake alone can add ~60 ms (3 network RTTs) of latency (Enable HTTPS keepAlive in Node.js – DEV Community). With Keep-Alive, after the first request, later requests can be 70% faster since they skip handshake steps (Enable HTTPS keepAlive in Node.js – DEV Community). Lower CPU and Network Overhead: Establishing a connection (especially HTTPS) is CPU-intensive. Reusing connections means fewer new connections to set up, which reduces CPU usage on both client and server (HTTP persistent connection – Wikipedia). It also means fewer packets overall (no repeated SYN/SYN-ACK), reducing network congestion (HTTP persistent connection – Wikipedia). Higher Throughput: For high-throughput scenarios (like microservices calling each other hundreds or thousands of times per second), using new connections for every request can exhaust the operating system’s resources. For instance, the OS may keep closed TCP ports in a TIME_WAIT state for ~60 seconds (When should I use (or not use) the keepAlive option of the Node.js http Agent? – Stack Overflow). If your Node.js process makes a huge number of short-lived requests without Keep-Alive, it can run out of ephemeral ports due to so many sockets in TIME_WAIT. Keep-Alive avoids this by reusing sockets, preventing port exhaustion (When should I use (or not use) the keepAlive option of the Node.js http Agent? – Stack Overflow). Fewer Open/Close Cycles: Opening and closing connections repeatedly is a waste of resources. Servers need to allocate memory and file descriptors for each connection. By keeping connections open, you reduce the churn of constantly allocating and freeing these resources. This is especially important in microservice architectures where services may communicate very frequently. In summary, HTTP Keep-Alive helps eliminate the overhead of repeated connections, which is why it’s on by default in HTTP/1.1 (and HTTP/2 takes it further with multiplexing). However, effectively using Keep-Alive in Node.js/NestJS requires some configuration to get the best performance. Enabling HTTP Keep-Alive on a NestJS Server NestJS applications (when using the default Express HTTP adapter) run on Node’s built-in HTTP server. Out of the box, Node’s HTTP server will allow persistent connections, but it has default timeout settings that may not be optimal for high-throughput use cases. In Node.js, the server’s default keep-alive timeout is only 5 seconds (Tuning HTTP Keep-Alive in Node.js). This means if a client doesn’t send a new request within 5 seconds after the last response, the server will close the connection. For many APIs, 5 seconds may be too short – especially if clients make bursts of requests with pauses slightly longer than 5 seconds, causing connections to drop and re-establish frequently. Tuning the server’s Keep-Alive settings: We can increase the keep-alive timeout on the NestJS server to keep connections open longer. NestJS gives us access to the underlying Node http.Server instance, which has properties to configure timeouts. For example, in your main.ts (bootstrap file) you can do the following: import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as http from 'http'; async function bootstrap() { const app = await NestFactory.create(AppModule); const server: http.Server = app.getHttpServer(); // Set Keep-Alive and header timeouts (in milliseconds) server.keepAliveTimeout = 30000; // 30 seconds keep-alive timeout server.headersTimeout = 31000; // 31 seconds headers timeout (should be a bit more than keepAliveTimeout) await app.listen(3000); } bootstrap(); In the above example, we set the server to keep idle HTTP connections open for 30 seconds. We also set the headersTimeout slightly higher (31 seconds) – Node.js recommends the headers timeout (maximum time to wait for the next request’s headers) be slightly greater than the keep-alive timeout (Tuning HTTP Keep-Alive in Node.js) (Tuning HTTP Keep-Alive in Node.js). This prevents the server from terminating a socket exactly as a client is sending a new request. The code is effectively the same as you would use in a plain Node.js HTTP server (NestJS tip: how to change HTTP server timeouts – DEV Community), but accessed through Nest’s app.getHttpServer(). Why increase keep-alive timeout? If your NestJS service is behind a load balancer or receives traffic in bursts, a longer keep-alive can reduce the need to frequently reconnect. For example, AWS Elastic Load Balancers have a default idle timeout of 60 seconds. If Node’s keep-alive timeout remained 5 seconds, the Node server would close connections much sooner than the ELB, possibly leading to “502 Bad Gateway” errors when the ELB tries to reuse a closed connection (Tuning HTTP Keep-Alive in Node.js). By configuring Node’s server.keepAliveTimeout to match or exceed the LB’s timeout (e.g. ~61 seconds for a 60s ELB timeout) (Tuning HTTP Keep-Alive in Node.js), you ensure the server doesn’t drop the connection first. In general, make sure the server’s keep-alive timeout is >= any client or proxy keep-alive intervals to avoid unexpected connection resets (Tuning HTTP Keep-Alive in Node.js). Note: Keep in mind that a very long keep-alive timeout means the server will hold many connections open, which could use more memory. If you have tens of thousands of clients, you might not want each to hold an idle connection for 2 minutes. Choose a timeout that balances reuse benefits against resource usage. For many high-throughput APIs, something on the order of 30–60 seconds is reasonable in production, but your needs may vary. If you’re using Fastify (the alternative HTTP adapter for NestJS), the same concept applies – Node’s HTTP server is still under the hood. You can similarly access the server and set keepAliveTimeout. (Fastify uses slightly different defaults but generally also supports persistent connections). Using HTTP Keep-Alive in HTTP Clients (Axios, http/https) for NestJS In addition to configuring your NestJS server, it’s equally important to enable Keep-Alive for outgoing HTTP requests made by your NestJS app. This is relevant if your NestJS service calls external APIs or microservices (e.g., Service A calls Service B’s REST API). By default, Node.js HTTP clients do not keep connections alive – the default HTTP agent opens a new TCP connection for each request, unless instructed otherwise (Reusing Connections with Keep-Alive in Node.js – AWS SDK for JavaScript) (HTTP | Node.js v23.11.0 Documentation). We need to explicitly enable Keep-Alive in our HTTP client libraries to reap the performance benefits. Using Axios (or NestJS HttpService) with Keep-Alive NestJS offers an HttpModule (built on Axios) to make HTTP requests. Axios, when run in Node.js, uses Node’s http/https module under the hood. By default, Axios does not reuse TCP connections unless you configure an HTTP agent with keepAlive. The Axios docs note that you can define a custom httpAgent and httpsAgent to enable options like keep-alive, which are not enabled by default (axios – NestJS). To use Keep-Alive with Axios in a NestJS service, you have two main options: Use Nest’s HttpModule with a custom Axios config: When importing the HttpModule, you can pass a configuration object that Nest will use to create the Axios instance. For example: import { HttpModule } from '@nestjs/axios'; import * as http from 'http'; import * as https from 'https'; @Module({ imports: [HttpModule.register({ timeout: 5000, // request timeout in ms (example) maxRedirects: 5, // Enable persistent connections: httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), })], // ...providers, controllers }) export class AppModule {} This configures Axios to use a keep-alive HTTP agent for both HTTP and HTTPS. Once this is set up, you can inject HttpService into your services and controllers, and any outbound HTTP call will reuse connections. For instance: @Injectable() export class ApiServic