← Back to list

Behind the Scenes: How Push Notifications Scale to Millions of Users

Introduction

WarsawJS in WarsawJS · 2025-04-16 19:08 · 0 claps · 4.3 min read
#push-notification #javascript #meetup #warsawjs #vonage
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Behind the Scenes: How Push Notifications Scale to Millions of Users

Introduction

Buzz. Ding. Pop. The sounds of our daily digital life have become so commonplace we hardly notice them anymore — until they stop, that is. Push notifications have become the invisible backbone of our connected experience, delivering everything from breaking news to chat messages, appointment reminders to security alerts.

But have you ever stopped to wonder what happens behind the scenes to make those little alerts appear on your screen? As Avital Tzubeli shared in her WarsawJS presentation, the scale is mind‑boggling: 16 million notifications per day, all racing against time, load, and latency to reach their destination.

In this article, we’ll dive into the fascinating world of push‑notification infrastructure, exploring how companies like Vonage build scalable message buses using Node.js, RabbitMQ, and intelligent auto‑scaling to keep those notifications flowing reliably.

Piotr and Avital during WarsawJS Meetup #78

Piotr and Avital during WarsawJS Meetup #78

Key Takeaways from the Presentation

1. The Scale Challenge

Push notifications operate at an astonishing scale. For a communications platform like Vonage, delivering 16 million notifications daily means their systems must process:

  • 185 notifications per second (average)
  • Thousands per second during peak hours
  • Destinations in virtually every country and time zone

This scale presents unique challenges around:

  • Reliability — every notification must reach its destination
  • Speed — users expect near‑instantaneous delivery
  • Resilience — the system can’t fail during traffic spikes

2. The Message‑Bus Architecture

At the core of any notification system is a message bus — essentially a highly optimized traffic controller for data. Vonage’s implementation uses a combination of:

  • Node.js for the service layer
  • RabbitMQ as the message broker
  • Auto‑scaling infrastructure to handle load fluctuations
  • Monitoring systems that track performance and failures

3. The Role of Monitoring and Alerting

Sophisticated monitoring is essential for maintaining a high‑performance notification system. Comprehensive telemetry helps:

  • Predict traffic patterns
  • Identify bottlenecks before they impact users
  • Alert teams to unusual behavior that might indicate issues
  • Provide data for continuous improvement

Technical Implementation Details

Below is a simplified example of how you might build a scalable notification pipeline.

Setting Up a Basic Message Queue with Node.js and RabbitMQ

producer.js — the service that creates notifications:

// producer.js
const amqp = require('amqplib');

async function sendNotification(userId, message, priority) {
  try {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();

    const queue = 'notifications';
    await channel.assertQueue(queue, { durable: true });

    const notification = {
      userId,
      message,
      timestamp: new Date().toISOString(),
      priority
    };

    channel.sendToQueue(queue, Buffer.from(JSON.stringify(notification)), {
      persistent: true
    });

    console.log(`✅ Notification queued for user ${userId}`);

    setTimeout(() => connection.close(), 500);
  } catch (error) {
    console.error('Failed to send notification:', error);
  }
}

consumer.js — the service that delivers notifications:

// consumer.js
const amqp = require('amqplib');

async function startConsumer() {
  try {
    const connection = await amqp.connect('amqp://localhost');
    const channel = await connection.createChannel();

    const queue = 'notifications';
    await channel.assertQueue(queue, { durable: true });
    channel.prefetch(1);

    console.log('🔄 Notification consumer waiting for messages…');

    channel.consume(queue, async (msg) => {
      if (!msg) return;

      const notification = JSON.parse(msg.content.toString());

      try {
        await sendToDevice(notification);
        channel.ack(msg);
        console.log(`✅ Delivered notification to user ${notification.userId}`);
      } catch (error) {
        channel.reject(msg, true); // requeue for retry
        console.error(`❌ Failed to deliver notification: ${error.message}`);
      }
    });
  } catch (error) {
    console.error('Consumer error:', error);
  }
}

async function sendToDevice(notification) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Math.random() > 0.05 ? resolve() : reject(new Error('Device unreachable'));
    }, 50);
  });
}

startConsumer();

Implementing Auto‑Scaling

A lightweight worker‑scaling manager:

// worker-manager.js
const { exec } = require('child_process');

class WorkerManager {
  constructor(queueUrl, minWorkers = 2, maxWorkers = 10) {
    this.queueUrl = queueUrl;
    this.minWorkers = minWorkers;
    this.maxWorkers = maxWorkers;
    this.currentWorkers = 0;
    this.workers = new Map();
  }

  async initialize() {
    for (let i = 0; i < this.minWorkers; i++) await this.startWorker();
    setInterval(() => this.adjustWorkerCount(), 30_000);
  }

  async getQueueMetrics() {
    // Replace with real metrics from RabbitMQ
    return {
      messageCount: Math.floor(Math.random() * 10_000),
      consumerCount: this.currentWorkers,
      avgProcessingTime: 50 + Math.random() * 20
    };
  }

  async adjustWorkerCount() {
    const { messageCount } = await this.getQueueMetrics();
    const target = Math.ceil(messageCount / (100 * 5));
    const desired = Math.max(this.minWorkers, Math.min(target, this.maxWorkers));

    if (desired > this.currentWorkers) {
      console.log(`Scaling up to ${desired} workers`);
      for (let i = this.currentWorkers; i < desired; i++) await this.startWorker();
    } else if (desired < this.currentWorkers) {
      console.log(`Scaling down to ${desired} workers`);
      for (let i = this.currentWorkers; i > desired; i--) await this.stopWorker();
    }
  }

  async startWorker() {
    const id = Date.now().toString(36) + Math.random().toString(36).slice(2);
    const proc = exec(`node consumer.js ${this.queueUrl} ${id}`);
    this.workers.set(id, proc);
    this.currentWorkers++;
    console.log(`Started worker ${id}. Total: ${this.currentWorkers}`);
  }

  async stopWorker() {
    if (this.currentWorkers <= this.minWorkers) return;
    const [id] = this.workers.keys();
    this.workers.get(id).kill('SIGTERM');
    this.workers.delete(id);
    this.currentWorkers--;
    console.log(`Stopped worker ${id}. Total: ${this.currentWorkers}`);
  }
}

new WorkerManager('amqp://localhost').initialize().catch(console.error);

Implementing Monitoring

A minimal Prometheus metrics endpoint:

// monitoring.js
const promClient = require('prom-client');
const express = require('express');
const amqp = require('amqplib');

const register = new promClient.Registry();
promClient.collectDefaultMetrics({ register });

const notificationsSent = new promClient.Counter({
  name: 'notifications_sent_total',
  help: 'Total notifications sent',
  labelNames: ['priority', 'status'],
  registers: [register]
});

const notificationLatency = new promClient.Histogram({
  name: 'notification_latency_seconds',
  help: 'Notification delivery time',
  buckets: [0.05, 0.1, 0.2, 0.5, 1, 2, 5],
  registers: [register]
});

const queueSize = new promClient.Gauge({
  name: 'notification_queue_size',
  help: 'Current queue depth',
  registers: [register]
});

async function updateQueueMetrics() {
  try {
    const conn = await amqp.connect('amqp://localhost');
    const ch = await conn.createChannel();
    const q = await ch.assertQueue('notifications');
    queueSize.set(q.messageCount);
    await conn.close();
  } catch (err) {
    console.error('Queue‑metrics update failed:', err);
  }
}

setInterval(updateQueueMetrics, 15_000);

express()
  .get('/metrics', async (_, res) => {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
  })
  .listen(9090, () => console.log('Metrics server on :9090'));

module.exports = {
  recordNotificationSent: (priority, status) =>
    notificationsSent.inc({ priority, status }),
  recordLatency: (seconds) => notificationLatency.observe(seconds)
};

Conclusion

Push notifications might seem simple on the surface, but building a system that reliably delivers millions of messages every day demands sophisticated architecture, careful monitoring, and intelligent scaling. As Avital Tzubeli demonstrated, combining Node.js, RabbitMQ, and cloud auto‑scaling offers a powerful foundation.

Key Lessons

  1. Design for resilience from the start — assume components will fail.
  2. Implement comprehensive monitoring — you can’t fix what you can’t measure.
  3. Use message queues to decouple systems — for scalability and fault tolerance.
  4. Auto‑scale based on real‑time metrics — react automatically to load changes.
  5. Prioritize messages appropriately — not every alert is equally urgent.

As our digital lives grow ever more notification‑driven, understanding these architectures gives developers invaluable insights into building any high‑scale, reliable messaging system.

Whether you’re building the next communications platform or “just” adding notifications to your app, these patterns will help you create resilient, scalable systems that keep users informed without missing a beat.

Have you implemented push notifications in your applications? What challenges did you face with scaling? Share your experiences in the comments below!


메타데이터
post_id
4651289e9d00
slug
behind-the-scenes-how-push-notifications-scale-to-millions-of-users-4651289e9d00
url
https://medium.com/warsawjs/behind-the-scenes-how-push-notifications-scale-to-millions-of-users-4651289e9d00
canonical_url
https://medium.com/warsawjs/behind-the-scenes-how-push-notifications-scale-to-millions-of-users-4651289e9d00
author_url
https://medium.com/@warsawjs
status
ok
fetched_at
2026-08-11 06:16:31