Scaling a Push Notification Server

Scaling a Push Notification Server

Previously we explored the topic of setting up a sandbox push notification server in Node.js. This featured a Mongodb instance to store users and device IDs, as well as endpoints to register users and send them the push notifications all at once. But what happens when you need to target individual devices instead of blasting your entire user base all at once? All of a sudden instead of processing one API call to send notifications to all of your users, you’re fielding multiple calls at once, possibly on the order of thousands of requests per second during peak user hours for your application. Fortunately, Node.js has built-in tools to help you scale your server.

The first issue we can tackle is breaking our Send endpoint out to work as a tool to send an individual push notification. We’ll assume in this case that the requests to this endpoint will come from another source with knowledge of the device Id and operating system to target. We’ll again use restify to set up our server:

 

//server.js

const restify = require('restify');

const server = restify.createServer({
name : 'pushTest'
});

// this allows us to parse the POST request body
server.use(restify.plugins.bodyParser());

server.listen(8080, () => {
console.log('listening on port 8080');
});

// set up a basic route
server.post('/', (req, res) => {
// parse the request body
let body = JSON.parse(req.body);

// check to make sure the body has the correct fields
if (body && body.platform) {
// send push here
}
// send a success response
res.send(200);
});

We’ll skip over actually sending the push since we covered that in our previous post. Once we start the server, we can use the ApacheBench command line tool to load test it. In a separate terminal window, paste:

ab -p test.json -c 20 -t 10 http://localhost:8080/

Where test.json is a local json file with test data. This will open up 20 connections per second for 10 seconds on our server. When we run this we get an output of about 150 successful requests per second, but let’s see if we can do better. The cluster module ships with node and allows us to spin up a server for every CPU we have on our machine. In a separate file we can have a “master” node that spins up servers for every CPU we have on our machine:

// master.js

const cluster = require('cluster');
const os = require('os');

// check if master
if (cluster.isMaster) {
// find out how many CPUs we have available
const cpuNum = os.cpus().length;

console.log(`Found ${cpuNum} CPUs`);

// fork the process for as many CPUs as we have
for (let i = 0; i < cpuNum; i++) {
cluster.fork();
}
} else {
// otherwise spin up server
require('./server');
}

Now instead of running

node server.js, run

node master.js

On my personal machine, this spins up eight different instances of the push server, and when I run our same ab command, I’m now seeing over 500 requests per second. This works by running master once and then running it again every time cluster.fork() is called for as many CPUs as we have. If master.js is entered as a result of calling fork, the isMaster call will fail and it will spin up the server.

This is a simple example of the built-in power of Node.js to increase the scalability of your application and expertly handle any kind of heavy load your test servers might need to endure.

John Surface

John Surface

Senior Developer

With a birth weight of just under seven and a half pounds, John has in less than three decades managed to gain thirteen stone and several years of experience as a full-stack and mobile engineer. He does his part to slow the spin of the earth spiraling out of control by creating robust backend solutions and intuitive cross-platform and native mobile applications.