How to run many Node.js HTTP servers concurrently using child processes by Tobias Quante

How to run many Node.js HTTP servers concurrently using child processes

Tobias Quante

Tobias Quante

Node.js applications run on a single thread. If you suddenly have to serve lots of clients at once, you're bound to run into problems with throughput. Learn how to mitigate these with a simple strategy: Using child processes!

Table of contents

Nowadays, everything is containerized.

This is generally a good thing, because:

  • There's a clear distinction between software components.
  • You can scale up and down as you like.
  • And do more stuff.

The cost of using Docker

These many benefits, however, induce hidden costs.

If you don't believe me, ask ChatGPT:
"What are possible technical costs of using container software such as docker?"

Sometimes you just want to keep it simple. By simple, I mean: Using Node core modules. There are process managers like pm2 or forever. Which bring their own

  • features
  • learning curve
  • complexity

And so forth. You see where I'm going.

No. Today, I just want to start many HTTP servers at once.

The 'child_process' module

You can achieve this with a Node core module called `child_process`. It allows you to execute Javascript files as a distinct system process.

This image illustrates the spawning (or forking) of a Node child process

This means: You can start as many web servers as you like (or have free ports). You should limit yourself to one process per available CPU on your machine for optimal results.

If you started more than one HTTP server per CPU core, they would cannibalize each others

So let's do it.

Write code for the server

We'll use the 'http' module to spin up a server that answers with the port it was started on. In reality, this server would be responsible to connect with a data layer. Or validate incoming requests.

The core difference is that we pass in the 'PORT' as a process variable. It's not declared inside the server module. Instead, the parent Node process must pass it down once it spawns its child process.

const http = require('http');
const PORT = process.argv[2]

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello from port ' + PORT);
})

server.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

Write the application's main-function

We'll use another Node core module to determine the count of CPUs of our machine. Then, it's time to spin up one server process for each available core.

const cp = require('child_process');
const os = require('os');
const cpuCount = os.cpus();

cpuCount.forEach((cpu, index) => {
	const PORT = `${4000 + index * 10}`
	console.log(`Starting server on port ${PORT} with cpu ${cpu.model}, speed ${cpu.speed}`);
	cp.fork('./server.js', [PORT]);
});

The port range starts from '4000' and increases by '10' for each additional server that starts. In my machine's case, since I have 8 CPU cores available, I'll see the following console output:

Which also means we're done. You can now try and visit these server URLs to validate if everything is working correctly.

How to proceed

Several servers at once are a good start to reduce server load. But there's more to be done.

By itself, the above method brings no real value. You probably don't want to modify your frontend app to decide what server it requests data from.

It's much easier - and good practice - to employ a load balancer. You could use an Nginx config like the following:

http {
    # Define upstream servers for load balancing
    upstream node_services {
        # ip_hash; # uncomment if you handle serverside sessions
        
        server http://localhost:4000;
        server http://localhost:4010;
        server http://localhost:4020;
        # ... more node services
    }   
}

server {
    listen 80;
    
    # Add a reverse proxy location
    location /api {
        proxy_pass http://node_services
    } 
} 

Finally, it's up to you how to structure your architecture. If you like to keep things simple, the `child_process` module is a great choice. When working on large scale applications, you will still want to use the industry's favorite tooling though.

Share this article