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.

Creating a Push Notification Server with Node.js

Creating a Push Notification Server with Node.js

At Shockoe we’re used to integrating our clients’ complicated backend systems into our apps, and this often includes push notifications. However, to get that initial proof that our app is playing nicely with GCM and APNS we’ll sometimes rig up a sandbox server of our own. Since we’re used to JavaScript, Node is an obvious go-to. Here are the steps for rigging up a simple push notification server in Node.js. If you’re looking for a deeper dive into messaging 2018, you can also check out Shockoe’s Innovative Brand Messaging in a World Noise page shown below — it’s just as important to know when to use push notifications, especially in today’s saturated push-notification environment.

How to Message in a World of Noise

Push notifications without a sound messaging strategy can be quickly damaging to a brand’s reputation. See how Shockoe is working with leading companies to build the right messaging tactics to delight the modern-day user.

Learn to Message in 2018

A push notification server consists of two parts: storing deviceIds and sending push notifications. To achieve this we’ll create two endpoints: register and send. Register will utilize the mongodb node module mongoose and Send will leverage two platform-specific node modules: apns and node-gcm. We’ll also be using restify to set up our endpoints.

Register

For a basic implementation we only need two pieces of information: the deviceId and platform (Android or iOS). Technically–in some mongoose schema code golf scenario–we just need the deviceId, and could make both calls with one always failing.

So our mongoose schema, with the proper requirements, would look something like this:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const Device = new Schema({
    deviceId : String,
    platform : String
});

const DeviceSchema = mongoose.model('Device', Device);

Mongoose is generally a lot more powerful than this implementation, with validations and enumerations that allow for easy error checking, but for the purposes of this demo we’ll use a basic schema. After making sure mongodb is installed and running on your machine, our restify endpoint would look something like this:

const restify = require('restify');

const db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));

db.once('open', function(){
    console.log('db open');
});

mongoose.connect('mongodb://localhost/pushserver');

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

server.use(restify.plugins.bodyParser());

server.post('/register', (req, res, next) => {
    let body = JSON.parse(req.body);

    if (body) {
        let newDevice = new DeviceSchema(body);
        newDevice.save(err => {
            if (!err) {
                res.send(200);
            } else {
                res.send(500);
            }
        }
    }
}

Ideally we would perform a find on DeviceSchema to make sure we’re not adding duplicate devices.

Send

For Send we’ll implement a basic endpoint that will send a push notification to every device in the database. We can use a simple GET, grab the devices from mongo, and call functions to send the push, based on whether the target is an iOS or an Android device. The Android push module can take an array of deviceId’s instead of one at a time, so we’ll push those ids onto an array and call the sendAndroid function on all of them.

server.get('/send', (req, res) => {
    DeviceSchema.find( (err, devices) => {
        if (!err && devices) {
            let androidDevices = [];
            devices.forEach(device => {
                if (device.platform === 'ios') {
                    sendIos(device.deviceId);
                } else if (device.platform === 'android') {
                    androidDevices.push(device.deviceId);
                }
            });
            sendAndroid(androidDevices);
            res.send(200);
        } else {
            res.send(500);
        }
    });
});

Ideally we would only send the 200 response once we were sure that every push had been sent successfully, but for now it will indicate that the send functions were called.

Our sendIos function will leverage the apns module, which takes an options object. Make sure to include your push key and certificate .pem files at the root of your project:

const apns = require('apns');

const options = {
    keyFile  : 'key.pem',
    certFile : 'cert.pem',
    debug    : true,
    gateway  : 'gateway.sandbox.push.apple.com',
    errorCallback : function(num, err) {
        console.error(err);
    }
};

function sendIos(deviceId) {
    let connection = new apns.Connection(options);

    let notification = new apns.Notification();
    notification.device = new apns.Device(deviceId);
    notification.alert = 'Hello World !';

    connection.sendNotification(notification);
}

For android we’ll use node-gcm. Make sure you have your api key:

const gcm = require('node-gcm');

function sendAndroid(devices) {
    let message = new gcm.Message({
        notification : {
            title : 'Hello, World!'
        }
    });

    let sender = new gcm.sender('<YOUR_API_KEY_HERE>');

    sender.send(message, {
        registrationTokens : devices
    }, function(err, response) {
        if (err) {
            console.error(err);
        } else {
            console.log(response);
        }
    });
}

After that it’s just a simple matter of running your project and you’re ready to hit these endpoints and test push notifications on your app!

Want to chat about Push in your App?

We’re all ears! We’ll help your team communicate with your audience more effectively through app-based push notifications. Give us a ring and our team of strategist and developers will discuss with you ways to bring your team’s strategy up to tomorrow’s standards.