Resource management in Node.js: the good, the bad and the worst

Resource management in Node.js: the good, the bad and the worst

In the previous article on resource management in Node.js, we covered the options available to manage resources. However, the previous article only provides a general overview.

In this article, we’ll see the pros and cons of using each of them. Spoiler: some of the options don’t make much sense to use.

Node.js CLI options

The Node.js CLI has two options for managing heap sizes: --max-old-space-size and --max-semi-space-size.

Trade-offs

Those options cannot regulate everything. Here a just a few cases where CLI options won’t work for you.

Spawned process. Whenever you spawn a new process via child_process.spawn, you’re creating a new instance of V8 alongside the process. It doesn’t inherit all of the options from the process that it was spawned from automatically. Sure, you can pass them manually, but now you have to be aware of each process and its memory consumption in the system.

Addons. Addons are not directly related to JavaScript. In fact, they are C++ libraries accessible through JavaScript. This means the code running inside those libraries is not affected by V8 restrictions, at least in this case.

I/O operations. I/O operations are handled by the libuv library and it means we’re facing C++ again. And not only that. When reading a large file, the result is passed as a buffer into the callback:

import fs from 'node:fs';

// The data is Buffer object.
fs.readFile('large-file.txt', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
  } else {
    console.log('File contents:', data.toString());
  }
});

Even when we have the buffer instance inside of JavaScript, the memory allocated for this buffer resides outside of the V8 heap and is, therefore, unaffected by the options.

When to use

Use those when you want to specifically imply the limits of JavaScript-related code consumption to a single process.

They make garbage collection more efficient. When memory consumption gets closer to the size that you provided, the garbage collection runs more frequently, resulting in less memory usage.

PM2 process manager

PM2 is a popular JavaScript process manager. It has a feature to restart a certain process based on the memory it consumes. You can achieve this by specifying the max_memory_restart option.

Trade-offs

The first and most significant tradeoff is how the process manager manages memory consumption. It has a separate worker process that checks memory consumption every 30 seconds.

This means it takes up to 30 seconds for the process that ran out of memory to be detected and restarted. These 30 seconds can cost a lot, up to the whole server crash.

The other is inherited problems that come with directly using PM2, such as:

  • Overselling clustering that leads to cumbersome workflow.

  • Attempts to go beyond a simple process manager and deliver on those attempts purely, compared to the alternatives.

  • Licensing issues.

I’ll soon publish an article that goes deep into the PM2 problems.

When to use

To be honest, I don’t see any good reason to use PM2 at this point except only one specific use-case that I’ll describe in the upcoming article.

User limits

User limit is the way to set limits to resources based on the user's role in a unix-based system.

Trade-offs

It is hard to scale. This type of limitation is especially challenging to scale if we want fine-grained control over specific processes or groups of processes.

Managing a large number of users, each with its own limits, quickly becomes complex.

When to use

It is an excellent option to use as a second line of defense. For example, if you have some way of managing resource consumption of a process or process group but want to be sure that if something goes wrong, you have a backup plan.

Control groups

Control groups are meant to manage resource consumption on the system level, similar to user limits in their focus on resource management.

Trade-offs

The only major problem I see with control groups is the configuration process. You might rightly say “skill issues.” However, the lack of clear interfaces through which we can configure them, like a single configuration file that resides next to the application code, makes it increasingly hard to manage.

One more issue could be a lack of complete isolation. With control groups, you’re still pretty much working on the same machine, with the shared file system, network, and any other resources.

When to use

If you have enough skill and understanding of how they work, you can use them whenever you want. They are flexible enough to deliver on most of the tasks related to resource management.

Containers

Container is an abstraction that goes further than control groups. It allows allocating resources for a group of processes and almost completely isolates them in terms of file system, network, etc.

Trade-offs

The main trade-off of containers is the abstraction itself. It adds more complexity to the whole workflow.

Such complexity results in:

  • Higher resource usage. Creating a container requires more resources than making a simple control group.

  • Performance problems in high-performance applications. As a result of high resource consumption, you can run into performance issues.

  • Learning curve.

When to use

Despite container trade-offs in particular cases, it is still the best solution for resource management we have so far. Here are just a few benefits that you get by using them:

  • Granular control. They heavily restrict resource usage (using control groups for it). Each container is isolated from one another, making it harder to break things up.

  • Better tooling. Tooling around containers allows you to configure containers for specific needs easily. Moreover, If you’re using some IDE with tools like Docker, it will hint you commands that you can use and highlight the ones that you can’t, making the experience even better.

  • Great isolation. Containers provide a great isolation for applications running inside of them.

Conclusion

There are many options for resource management of Node.js applications. It all comes down to understanding the trade-offs of each approach and your specific needs.

In general, I would stick with containers whenever possible. Even if you don't know about them much, it is a great opportunity to learn.