PM2: Run cron-job from single process in cluster mode

PM2 is a process manager for node.js application. It helps to manage and keep an application online for 24/7. In PM2 there is an option to run the code in cluster mode. The cluster mode allows node.js applications to be scaled across all CPUs available, without any code modifications. So this is a great feature in pm2.

In another side cron is a scheduler program which we can use to schedule some of our task/code to run on time basis.

In our project these two (PM2 and cron) are implemented.

Now I want to share a story about some issues we’ve faced regarding these and how we solved them.

In our project we used cron to run some scheduled task/code at the day end. The goal of the scheduled task was to take the summary of some data from a collection and insert them into another collection.

So this works fine if we run the code in fork mode (single process).

But the problem occurs when we run the project using pm2 in cluster mode. Then all the cluster instances were being enabled to run the cron-jobs.

We couldn’t distinguish the instances to run cron-jobs only from a single one. As a result all the instances were running the scheduled task and inserting the same data into database.

The problem was severe because we needed to run multiple such cron-jobs and some of them are scheduled at the same time. And also now we want to scale our pm2 cluster instance. Because otherwise we couldn’t utilize our server capacity and reduce API response time.

And again, if we scale the cluster instances without solving the problem, then at the scheduled time all the instances will be busy to complete the scheduled task. No instance will be left to serve the (API response).

So our goal was to scale up the number of instances and also to manage the cron-job from a single one.

I’ve searched for this problems solution but nothing was satisfactory to me. Some solution suggests that pm2 passes the process id in environment variable as process.env.p_id.

So we could consider p_id=0 as a dedicated instance for running cron-jobs. Yes, that’s a solution.

But there was another problem in our case. Our server runs more than one application using pm2. And also p_id depends on the number of instances already running or registered in pm2.

So we are not sure about that p_id = 0 will always be available.

So how we solved the problem…

In PM2 there is an option to give a name to the running instance. And also in code, we can get the name from env variable. To access the name we need to write process.env.name. (Look, here name is custom (user-defined), but p_id is not.)

And also in pm2 configuration file we can pass an array of application configuration to run.

We’ve used both of them to solve our problem.

This was pm2 configuration file when the issue existed. (Some variables are skipped for brevity.)

Here’s our projects pm2 config file
Projects previous pm2 configuration

So, here we can pass more than one configuration in apps array. That’s what we did. We’ve passed the same configuration twice in apps array. And changed the name property in them. One is primary and the other one is replica.

And also updated the “instances”: “1” for primary and “3” for replica.

So now the configuration becomes like this

Now if we run our application using this configuration.

pm2 start pm2.json

We will see total 4 instances are running. One primary and 3 replica.

And also in our code, we can log the instance name to see.

console.log(process.env.name);

So now we can easily distinguish the instances by checking the process name. So we have modified our code to achieve our second goal (run cron job from single instance).

const processName = process.env.name;
if(processName === 'primary'){
console.log(processName, "instance will run cron job.");
RunCronJob();}

Boom!! Now cron job will run from primary instance only, and the other instances (3 replica) will not.

Up-to now we scaled our server and also managed to run cron jobs form a single (primary) instance.

The story doesn’t end here!!

We do not always want the hard-coded instance number in configuration file. We may want to run as much instance as possible. In our initial configuration instance:”max” was set. Which means as much possible instance can be created.

So how will we achieve that now!?

Well, there’s another nice feature in PM2 configuration. If we pass “instance”:”-1” then it considers it as max-1.

So now, if we write “instance”: “-1” in our replica configuration, then PM2 will create max-1 replica instance. And remember, we already have a primary instance. So in total max number of instance will be created, which is our initial configurations outcome!

Here is the updated configuration.

Replicated same configuration

Boom!!! Now we can run the maximum number of instance using PM2. And in bonus now we can manage the cron-job from a single instance!!

But this is not the end!!!

Our project’s pm2 configuration had environment based different parameters (like port, name etc). So we needed a more generalized solution so that our code works in whatever environment it is run. We needed to ensure only a single dedicated instance for cron-job in each environment.

To solve this, we implemented a little trick here. We gave some smart name to the instances. We gave environment based names like prod-primary, prod-replica, dev-primary, dev-replica…etc. And general name was primary, replica if no environment is passed.

Final configuration

Found any pattern?

Yes!! Now each instance name contains primary or replica. So whichever environment our code is running we must get primary or replica in process.env.name!

So we’ve updated the logic a bit little in our code. We were checking by regular expression. If the process name contains primary in it as regular expression then it is the primary instance! And we also gave a default name so that our code also runs without pm2.

const processName = process.env.name || 'primary';if(processName.search(/primary/) !== -1){    RunCronJob();
}

Yeah! Finally Done!!!

Happy Coding!!

Associate software engineer @BrainStation-23 since 2020