Build a Bulk SMS Broadcast System with MessageBird, Node.js & Fastify | Complete 2025 Guide
Learn how to build a production-ready bulk SMS system using MessageBird API, Fastify, BullMQ, and Redis. Step-by-step tutorial with code examples for sending high-volume SMS broadcasts, implementing job queues, and handling thousands of messages reliably.
Learn how to build a production-ready bulk SMS broadcasting system using MessageBird API with Node.js and Fastify. This comprehensive guide shows you how to create a scalable SMS broadcast application that efficiently handles high-volume messaging using asynchronous job queues with BullMQ and Redis. You'll implement the MessageBird Node.js SDK, build a high-performance Fastify REST API, and configure background workers for reliable SMS delivery at scale.
This step-by-step tutorial walks you through building a complete Node.js application using the Fastify framework to send bulk or broadcast SMS messages via the MessageBird API. You'll cover everything from project setup and core logic to asynchronous processing, error handling, and production deployment best practices.
Create a system that efficiently handles sending large volumes of SMS messages without blocking your main application thread, incorporating best practices for reliability and scalability. Perfect for marketing campaigns, notification systems, and mass communication needs.
Technologies Used:
Node.js: JavaScript runtime environment
Fastify: High-performance, low-overhead web framework for Node.js (v5.6+ as of October 2025). Chosen for its speed, extensibility, and developer-friendly features like built-in validation and logging
MessageBird: Communication platform-as-a-service (CPaaS) provider for sending SMS messages via their API
MessageBird Node.js SDK (messagebird): Official Node.js client that simplifies interaction with the MessageBird REST API
Redis: In-memory data structure store, used here as a message broker for background job processing
BullMQ: Robust Node.js library for creating and processing background jobs using Redis (v5.60+ as of October 2025). Essential for handling bulk operations asynchronously
Prisma: Next-generation ORM (Object-Relational Mapping) for Node.js and TypeScript, used for database interaction (optional but recommended for tracking)
PostgreSQL: Powerful, open-source relational database (optional, for use with Prisma)
dotenv: Module to load environment variables from a .env file
A Fastify API endpoint that accepts bulk SMS requests
An asynchronous job queue system (BullMQ + Redis) to process messages in the background
A background worker process that uses the MessageBird SDK to send SMS messages reliably
(Optional) Database integration with Prisma to track the status of each message
Configuration for error handling and retries
Prerequisites:
Node.js: v20 or later installed (v22 LTS recommended as of October 2024). Note: Node.js v18 reached End-of-Life on April 30, 2025, and is no longer supported
npm or yarn: Package manager for Node.js
MessageBird Account: Sign up for free at MessageBird
MessageBird API Access Key: Obtain from your MessageBird Dashboard (Developers → API access)
Redis Server: Running locally or accessible remotely (e.g., via cloud provider or Docker)
(Optional) PostgreSQL Database: Running locally or accessible remotely
Basic understanding of Node.js, APIs, and asynchronous programming
A verified phone number or purchased virtual number in MessageBird to use as the originator
messagebird: The official MessageBird Node.js SDK (formerly referenced as @messagebird/api in some examples)
bullmq: The job queue library
ioredis: A robust Redis client needed by BullMQ
dotenv: Loads environment variables from a .env file
prisma, @prisma/client, pg: For database interaction (ORM, client, PostgreSQL driver)
nodemon: Automatically restarts the server during development on file changes
concurrently: Runs multiple commands concurrently (useful for running the API and worker)
1.3 Project Structure
Create the following directory structure within your project root:
Bash
mkdir -p src/{config,jobs,routes,services,utils}
Your project structure:
Text
fastify-messagebird-bulk/
├── prisma/ # (Optional) Prisma schema and migrations
├── src/
│ ├── config/ # Configuration files (e.g., MessageBird, Redis)
│ ├── jobs/ # BullMQ job definitions and processors
│ ├── routes/ # Fastify API route definitions
│ ├── services/ # Business logic (e.g., MessageBird service wrapper)
│ ├── utils/ # Utility functions
│ ├── api.js # Fastify server entry point
│ └── worker.js # BullMQ worker entry point
├── .env # Environment variables (DO NOT COMMIT)
├── .env.example # Example environment variables
├── .gitignore
└── package.json
1.4 Configure Environment Variables
Create a .env file in the project root. This file holds your sensitive credentials and configuration. Never commit this file to version control.
Also create a .env.example file to show the required variables.
.env.example:
.env
# MessageBird Configuration
MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_ACCESS_KEY
MESSAGEBIRD_ORIGINATOR=YOUR_SENDER_ID_OR_NUMBER # e.g., +12025550147 or "MyCompany"
# Redis Configuration (for BullMQ)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# REDIS_PASSWORD=your_redis_password # Uncomment if your Redis requires auth
# REDIS_URL=redis://user:password@host:port # Alternative connection string
# Database Configuration (Optional – for Prisma)
# Example for PostgreSQL
DATABASE_URL="postgresql://user:password@host:port/database_name?schema=public"
# Application Configuration
API_PORT=3000
API_HOST=0.0.0.0 # Listen on all available network interfaces
QUEUE_NAME=sms_sending_queue
Copy .env.example to .env and fill in your actual credentials:
Bash
cp .env.example .env
MESSAGEBIRD_API_KEY: Your live API access key from the MessageBird dashboard (Developers → API access → Show key)
MESSAGEBIRD_ORIGINATOR: The sender ID for your SMS messages. This can be:
A purchased virtual mobile number (VMN) from MessageBird (e.g., +12025550147)
An Alphanumeric Sender ID (e.g., MyCompany, max 11 characters). Note: Alphanumeric IDs are not supported in all countries (e.g., the US) and cannot receive replies. Check MessageBird's country restrictions. Use a number for broader compatibility
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD / REDIS_URL: Connection details for your Redis server
DATABASE_URL: (Optional) Connection string for your PostgreSQL database if using Prisma. Follow the format specified in the Prisma documentation for your specific database
API_PORT, API_HOST: Network configuration for the Fastify server
QUEUE_NAME: A name for the BullMQ job queue
1.5 Configure .gitignore
Create a .gitignore file in the root to prevent sensitive files and unnecessary folders from being committed:
.gitignore:
Text
# Dependencies
/node_modules
# Environment Variables
.env
# Prisma
# Prisma migration files (*.sql within migration folders) define schema changes and should be committed.
# Ignore SQLite files if used during development
*.db
*.db-journal
# Logs
*.log
# Build Output (if any)
/dist
# OS generated files
.DS_Store
Thumbs.db
1.6 (Optional) Initialize Prisma
If you're using the database for tracking, initialize Prisma:
Bash
npx prisma init --datasource-provider postgresql
This creates the prisma/ directory and a schema.prisma file.
Example Prisma Schema for Message Tracking:
Update prisma/schema.prisma:
Prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model MessageLog {
id Int @id @default(autoincrement())
jobId String? @unique
recipient String
body String
status String @default("pending") // pending, processing, sent, failed
messageBirdId String?
failureReason String?
correlationId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Run the migration:
Bash
npm run db:migrate
1.7 Configure package.json Scripts
Add scripts to your package.json for easier development and execution:
start:api / start:worker: Run the API server and worker in production
dev:api / dev:worker: Run the API server and worker using nodemon for development (auto-restarts on changes)
dev: Runs both the API and worker concurrently for development using concurrently
db:migrate, db:studio: (Optional) Prisma commands for database migrations and GUI
Your basic project structure and configuration are now complete.
2. Implementing MessageBird API Integration and Job Queue
Implement the core logic for sending messages via MessageBird and set up the asynchronous queue.
2.1 Configure MessageBird Service for SMS Sending
Create a service file to encapsulate MessageBird interactions.
src/services/messagebirdService.js:
JavaScript
const messagebird = require('messagebird');
const dotenv = require('dotenv');
dotenv.config(); // Load .env variablesconst apiKey = process.env.MESSAGEBIRD_API_KEY;
const originator = process.env.MESSAGEBIRD_ORIGINATOR;
if (!apiKey) {
console.error('ERROR: MESSAGEBIRD_API_KEY environment variable not set.');
process.exit(1); // Exit if key is missing
}
if (!originator) {
console.warn('WARN: MESSAGEBIRD_ORIGINATOR environment variable not set. Using default or MessageBird assigned number might occur.');
}
let messagebirdClient;
try {
// Initialize the MessageBird client with your API key
messagebirdClient = messagebird(apiKey);
console.log('MessageBird SDK initialized successfully.');
} catch (error) {
console.error('ERROR: Failed to initialize MessageBird SDK:', error);
process.exit(1);
}
/**
* Sends a single SMS message using the MessageBird API.
*
* @param {string} recipient - The recipient's phone number (E.164 format, e.g., +12025550147).
* @param {string} body - The text message content.
* @returns {Promise<object>} - A promise that resolves with the MessageBird API response.
* @throws {Error} - Throws an error if the API call fails.
*/constsendSms = async (recipient, body) => {
if (!messagebirdClient) {
thrownewError('MessageBird SDK is not initialized.');
}
if (!recipient || !body) {
thrownewError('Recipient and body are required for sending SMS.');
}
const params = {
originator: originator,
recipients: [recipient],
body: body,
};
console.log(`Attempting to send SMS to ${recipient} via MessageBird…`);
// Using Promise wrapper for the callback-based APIreturnnewPromise((resolve, reject) => {
messagebirdClient.messages.create(params, (err, response) => {
if (err) {
console.error(`ERROR sending SMS to ${recipient}:`, JSON.stringify(err, null, 2));
reject({
message: `Failed to send SMS via MessageBird to ${recipient}`,
details: err.errors || [err]
});
} else {
console.log(`SUCCESS: MessageBird API response for ${recipient}:`, JSON.stringify(response, null, 2));
resolve(response);
}
});
});
};
module.exports = {
sendSms,
messagebirdClient // Export client if needed elsewhere (e.g., for balance checks)
};
Initialization: Loads the API key and originator from environment variables and initializes the MessageBird client using the messagebird package. Includes error handling for missing keys or initialization failures
sendSms function:
Takes recipient and body as arguments
Constructs the params object required by messagebirdClient.messages.create
Uses a Promise to handle the callback-based MessageBird SDK API
Logs attempts, successes, and failures with detailed error information
Rejects the promise with a structured error object for better upstream handling
Note on Bulk Sending: For sending to multiple recipients efficiently, MessageBird also provides a Batch API that allows sending different messages to up to 50 recipients per request. This can be more efficient than individual calls for very large batches.
2.2 Configure Redis and BullMQ for Asynchronous SMS Processing
Set up the connection to Redis and define the BullMQ queue.
src/config/redis.js:
JavaScript
constRedis = require('ioredis');
const dotenv = require('dotenv');
dotenv.config();
const redisConfig = {
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
// password: process.env.REDIS_PASSWORD, // Uncomment if neededmaxRetriesPerRequest: null, // Recommended by BullMQ docs
};
// Support for Redis URL connection stringconst redisUrl = process.env.REDIS_URL;
let connection;
if (redisUrl) {
connection = newRedis(redisUrl, { maxRetriesPerRequest: null });
console.log(`Connecting to Redis using URL: ${redisUrl.split('@')[1] || redisUrl}`); // Avoid logging password
} else {
connection = newRedis(redisConfig);
console.log(`Connecting to Redis at ${redisConfig.host}:${redisConfig.port}`);
}
connection.on('connect', () => {
console.log('Successfully connected to Redis.');
});
connection.on('error', (err) => {
console.error('Redis connection error:', err);
});
module.exports = connection;
Loads Redis connection details from .env
Provides flexibility for connecting via host/port or a REDIS_URL
Includes basic connection logging and error handling
src/jobs/smsQueue.js:
JavaScript
const { Queue } = require('bullmq');
const redisConnection = require('../config/redis');
const dotenv = require('dotenv');
dotenv.config();
constQUEUE_NAME = process.env.QUEUE_NAME || 'sms_sending_queue';
// Create a new Queue instance// Reuse the connection objectconst smsQueue = newQueue(QUEUE_NAME, {
connection: redisConnection,
defaultJobOptions: {
attempts: 3, // Number of times to retry a failed jobbackoff: {
type: 'exponential', // Exponential backoff strategydelay: 1000, // Initial delay in ms (1 second)
},
removeOnComplete: true, // Automatically remove job after completionremoveOnFail: false, // Keep failed jobs for inspection
},
});
console.log(`BullMQ queue "${QUEUE_NAME}" initialized.`);
// Optional: Listen for queue events globally if needed (e.g., for monitoring)
smsQueue.on('error', (error) => {
console.error(`Queue "${QUEUE_NAME}" error:`, error);
});
smsQueue.on('waiting', (jobId) => {
// console.log(`Job ${jobId} is waiting in queue "${QUEUE_NAME}".`);
});
smsQueue.on('active', (job) => {
// console.log(`Job ${job.id} has started processing in queue "${QUEUE_NAME}".`);
});
smsQueue.on('completed', (job, result) => {
// console.log(`Job ${job.id} completed successfully in queue "${QUEUE_NAME}". Result:`, result);
});
smsQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed in queue "${QUEUE_NAME}". Attempt ${job.attemptsMade}/${job.opts.attempts}. Error:`, err);
});
/**
* Adds a job to send a single SMS message to the queue.
* @param {string} recipient - The recipient's phone number.
* @param {string} body - The message content.
* @returns {Promise<import('bullmq').Job>} - A promise that resolves with the added job.
*/constaddSmsJob = async (recipient, body) => {
if (!recipient || !body) {
thrownewError('Recipient and body are required to add SMS job.');
}
console.log(`Adding SMS job to queue for recipient: ${recipient}`);
const jobData = { recipient, body };
const jobName = 'send-single-sms';
try {
const job = await smsQueue.add(jobName, jobData);
console.log(`Job ${job.id} added to queue "${QUEUE_NAME}" for recipient ${recipient}.`);
return job;
} catch (error) {
console.error(`Error adding job to queue "${QUEUE_NAME}":`, error);
throw error;
}
};
module.exports = {
smsQueue,
addSmsJob,
QUEUE_NAME
};
Initialization: Creates a Queue instance, connecting it to Redis via the shared connection
Queue Events: Includes listeners for various queue events for logging and monitoring
addSmsJob function: Adds a new job to the queue with recipient and body data
2.3 Create the Background Worker for SMS Job Processing
This process listens to the queue and executes the jobs.
src/worker.js:
JavaScript
const { Worker } = require('bullmq');
const redisConnection = require('./config/redis');
const { QUEUE_NAME } = require('./jobs/smsQueue');
const { sendSms } = require('./services/messagebirdService');
// Optional: Prisma client for database updates// const { PrismaClient } = require('@prisma/client');// const prisma = new PrismaClient();console.log(`Starting worker for queue "${QUEUE_NAME}"…`);
// Define the processing function for jobsconstprocessSmsJob = async (job) => {
const { recipient, body } = job.data;
console.log(`Processing job ${job.id} (Attempt ${job.attemptsMade + 1}/${job.opts.attempts}): Send SMS to ${recipient}`);
try {
// Optional: Update database status to 'processing' before sending// await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'processing' } });const result = awaitsendSms(recipient, body);
// Optional: Update database status to 'sent' on success// await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'sent', messageBirdId: result.id } });console.log(`Job ${job.id} completed successfully for ${recipient}. MessageBird Response ID: ${result?.id || 'N/A'}`);
return { messageBirdId: result?.id, status: 'sent' };
} catch (error) {
console.error(`Job ${job.id} failed for recipient ${recipient}:`, error.message || error);
// Optional: Update database status to 'failed'// await prisma.messageLog.update({// where: { jobId: job.id },// data: { status: 'failed', failureReason: JSON.stringify(error.details || error.message) }// });// IMPORTANT: Throw the error again to signal BullMQ that the job failed// This allows BullMQ to handle retries based on the queue's configuration.throw error;
}
};
// Create a new Worker instanceconst worker = newWorker(QUEUE_NAME, processSmsJob, {
connection: redisConnection,
concurrency: 5, // Process up to 5 jobs concurrentlylimiter: { // Optional: Rate limiting to avoid hitting MessageBird limitsmax: 10, // Max 10 jobsduration: 1000// per 1000 ms (1 second)
}
});
// --- Worker Event Listeners ---
worker.on('completed', (job, returnValue) => {
// console.log(`Worker completed job ${job.id} for queue "${QUEUE_NAME}". Return value:`, returnValue);
});
worker.on('failed', (job, err) => {
console.error(`Worker failed job ${job.id} for queue "${QUEUE_NAME}" after ${job.attemptsMade} attempts. Error:`, err.message || err);
});
worker.on('error', (err) => {
console.error(`Worker error for queue "${QUEUE_NAME}":`, err);
});
worker.on('active', (job) => {
// console.log(`Worker started processing job ${job.id} for queue "${QUEUE_NAME}".`);
});
worker.on('progress', (job, progress) => {
// If your job reports progress// console.log(`Worker progress for job ${job.id}:`, progress);
});
console.log(`Worker listening for jobs on queue "${QUEUE_NAME}" with concurrency ${worker.opts.concurrency}.`);
// Graceful shutdownconstgracefulShutdown = async (signal) => {
console.log(`\nReceived ${signal}. Closing worker and Redis connection…`);
await worker.close();
await redisConnection.quit();
// Optional: Close database connection// await prisma.$disconnect();console.log('Worker and connections closed gracefully.');
process.exit(0);
};
process.on('SIGTERM', () =>gracefulShutdown('SIGTERM'));
process.on('SIGINT', () =>gracefulShutdown('SIGINT')); // Catches Ctrl+C
Initialization: Creates a Worker instance listening to the queue
processSmsJob Function: The core job execution logic, calling sendSms. Handles errors and re-throws them for BullMQ retries. Includes optional Prisma integration points
Concurrency & Rate Limiting: Configures how many jobs run in parallel and applies rate limiting for MessageBird API calls
Event Listeners: Logs worker events
Graceful Shutdown: Ensures clean shutdown on termination signals
3. Building the Fastify REST API for Bulk SMS
Create the Fastify server and the API endpoint to receive bulk SMS requests.
Server Start: Standard Fastify server startup logic
4. MessageBird API Integration Guide
The core integration with MessageBird happens within the src/services/messagebirdService.js file, which was created in Section 2.1.
Key Integration Points:
Initialization:messagebird(process.env.MESSAGEBIRD_API_KEY) securely initializes the SDK using the API key from environment variables
Sending:messagebirdClient.messages.create(params, callback) is the method used to send individual SMS messages. The sendSms function wraps this logic
Configuration:MESSAGEBIRD_ORIGINATOR environment variable determines the sender ID
Credentials: Obtain API keys from the MessageBird Dashboard → Developers → API access. Use your LIVE key for actual sending. Store it securely in the .env file
Rate Limits: MessageBird's Reporting API has a rate limit of 5 requests per second. While specific SMS API rate limits are not publicly documented, implement rate limiting in your worker (see Section 2.3) to avoid throttling
You will see sections for Live API Key and Test API Key
Click Show key next to the Live API Key
Copy this key and paste it as the value for MESSAGEBIRD_API_KEY in your .env file
Important: Keep this key confidential. Do not commit it to version control
Environment Variable Summary:
Variable
Purpose
Format
How to Obtain
MESSAGEBIRD_API_KEY
Authenticates your application with the MessageBird API
Alphanumeric string (e.g., live_xxxxx...)
MessageBird Dashboard → Developers → API access → Live API Key
MESSAGEBIRD_ORIGINATOR
Sets the sender ID displayed on the recipient's device
E.164 phone number (e.g., +12025550147) or alphanumeric (max 11 chars, e.g., MyCompany)
Use a purchased/verified MessageBird number or register an alphanumeric sender ID
Alternative for Large Volumes: For larger volumes (50+ recipients with same message), consider using the MessageBird Batch API which allows up to 50 recipients per request for improved efficiency.
5. Running Your Bulk SMS Application
Start your bulk SMS system using the npm scripts configured earlier.
5.1 Development Mode
Run both the API server and worker concurrently with auto-restart on file changes:
Bash
npm run dev
Or run them separately in different terminals:
Bash
# Terminal 1: API Server
npm run dev:api
# Terminal 2: Background Worker
npm run dev:worker
5.2 Production Mode
Run the API and worker as separate processes:
Bash
# Terminal 1 or Process Manager: API Server
npm run start:api
# Terminal 2 or Process Manager: Background Worker
npm run start:worker
Process Manager Recommendation: Use a process manager like PM2 for production deployments:
Bash
# Install PM2 globally
npm install -g pm2
# Start both processes
pm2 start src/api.js --name "sms-api"
pm2 start src/worker.js --name "sms-worker"# View logs
pm2 logs
# Save process list for restart
pm2 save
pm2 startup
{"message":"Accepted 2 SMS messages for background processing.","jobCount":2,"correlationId":"test-batch-001","queueName":"sms_sending_queue"}
Monitor the worker logs to see job processing in real-time.
6. Next Steps and Best Practices
6.1 Security Best Practices
API Authentication: Add authentication middleware (e.g., API keys, JWT) to protect your /send-bulk endpoint
Rate Limiting: Implement rate limiting on API endpoints using @fastify/rate-limit to prevent abuse
Input Sanitization: Validate and sanitize all user inputs beyond schema validation
HTTPS: Always use HTTPS in production to encrypt data in transit
Environment Variables: Never commit .env files. Use secret management services in production (e.g., AWS Secrets Manager, HashiCorp Vault)
6.2 Monitoring and Observability
Logging: The built-in Pino logger provides structured JSON logs. Send these to a log aggregation service (e.g., ELK Stack, Datadog)
Metrics: Track key metrics like job success/failure rates, queue depth, and API response times
Alerting: Set up alerts for critical failures (e.g., Redis connection loss, MessageBird API errors)
BullMQ Dashboard: Use BullBoard to visualize your queues
6.3 Performance Optimization
Tune Concurrency: Adjust the concurrency setting in your worker based on your server resources and MessageBird rate limits
Use Batch API: For large volumes with identical messages, switch to MessageBird's Batch API
Database Connection Pooling: If using Prisma, ensure proper connection pool configuration
Redis Persistence: Configure Redis persistence (RDB or AOF) to prevent job loss on Redis restarts
6.4 Error Handling and Recovery
Dead Letter Queue: Configure a DLQ for jobs that fail after all retry attempts
Manual Retry Interface: Build an admin interface to manually retry failed jobs
Webhook Integration: Set up MessageBird delivery report webhooks to track message delivery status
6.5 Cost Optimization
Message Segmentation: Keep messages under 160 characters to avoid multi-part SMS charges
Test Mode: Use MessageBird's test API key during development to avoid charges
Monitor Usage: Regularly check your MessageBird dashboard for usage and billing information
Conclusion
You now have a production-ready bulk SMS broadcast system using MessageBird, Node.js, Fastify, and BullMQ. This architecture handles large message volumes efficiently through asynchronous processing, includes retry logic for reliability, and provides a foundation for scaling.
The key architectural decisions – using BullMQ for job queuing, Redis for persistence, and Fastify for high-performance API handling – create a robust system that can grow with your needs.
Use the Fastify framework with the MessageBird API and a Redis queue. The Fastify app receives SMS requests, adds them to a BullMQ job queue managed by Redis, and a background worker processes these jobs to send messages via the MessageBird API asynchronously.
The MessageBird Node.js SDK (@messagebird/api) simplifies interaction with the MessageBird REST API, making it easier to send SMS messages within your Node.js application. It handles the low-level details of making API calls, managing responses, and error handling.
Redis and BullMQ provide an asynchronous job queue system. This prevents blocking the main application thread when sending large volumes of SMS messages, improving responsiveness and scalability. BullMQ uses Redis as its message broker.
Prisma is optional but recommended for tracking message status. Use it if you need a robust way to persist message data (e.g., recipient, status, delivery reports) in a database like PostgreSQL.
Store your MessageBird API key securely in a .env file in your project's root directory. The provided code example uses dotenv to load environment variables, so your key will be available via process.env.MESSAGEBIRD_API_KEY. Never commit your .env file.
The example Fastify route includes error handling for queue failures using try-catch blocks around each addSmsJob call. Failed jobs are recorded and logged. The route is designed to continue processing and enqueueing other messages even if some queue additions fail.
The background worker listens to the Redis queue (using BullMQ) for new SMS sending jobs. It processes each job, extracting recipient and message data, and makes calls to the MessageBird API to send the actual SMS messages.
The worker example includes a limiter option with max and duration to control the number of jobs processed within a specified time window. Adjust these values according to your MessageBird account's specific rate limits to avoid exceeding them.
The guide recommends Node.js v18 or later. This ensures compatibility with Fastify, BullMQ, the MessageBird SDK, and other dependencies.
You can use a purchased Virtual Mobile Number (VMN) or an Alphanumeric Sender ID (if available in your target region). VMNs allow two-way communication (receiving replies), while Alphanumeric IDs (e.g., company name) might have country restrictions and don't receive replies. Configure it using the MESSAGEBIRD_ORIGINATOR environment variable.
Fastify is a high-performance Node.js web framework. It is chosen for its speed, extensibility, and developer-friendly features like schema validation and logging, which make it suitable for building robust and scalable APIs.
Create a config/redis.js file to set up the Redis connection. Provide your Redis host, port, and password (if necessary) using environment variables loaded with dotenv. Alternatively, you can use the REDIS_URL environment variable with a connection string.
The .gitignore file excludes sensitive information (like .env) and unnecessary files (like node_modules) from your Git repository. This is crucial for security and avoids bloating your project's version history.
Yes, Prisma supports various databases (PostgreSQL, MySQL, SQLite, and more). You'll need to adjust the datasource provider and connection string in your Prisma schema and .env file accordingly.
Use the concurrently package and the "dev" script defined in package.json. This script simultaneously runs both the API server with nodemon (auto-restarts) and the worker process with nodemon.
Learn how to build a production-ready bulk SMS broadcasting system using MessageBird API with Node.js and Fastify. This comprehensive guide shows you how to create a scalable SMS broadcast application that efficiently handles high-volume messaging using asynchronous job queues with BullMQ and Redis. You'll implement the MessageBird Node.js SDK, build a high-performance Fastify REST API, and configure background workers for reliable SMS delivery at scale.
This step-by-step tutorial walks you through building a complete Node.js application using the Fastify framework to send bulk or broadcast SMS messages via the MessageBird API. You'll cover everything from project setup and core logic to asynchronous processing, error handling, and production deployment best practices.
Create a system that efficiently handles sending large volumes of SMS messages without blocking your main application thread, incorporating best practices for reliability and scalability. Perfect for marketing campaigns, notification systems, and mass communication needs.
Technologies Used:
messagebird): Official Node.js client that simplifies interaction with the MessageBird REST API.envfileSystem Architecture:
Here's how the components interact:
What You'll Build:
By the end of this guide, you will have:
Prerequisites:
originatorInstall Redis using one of these methods:
# Docker (recommended for development) docker run --name redis -p 6379:6379 -d redis # macOS with Homebrew brew install redis brew services start redis # Ubuntu/Debian sudo apt update sudo apt install redis-server sudo systemctl start redis1. Setting Up Your Node.js Bulk SMS Project
Initialize your Node.js project and install the necessary dependencies for building a MessageBird SMS integration with Fastify.
1.1 Create Project Directory and Initialize
Open your terminal and create a new directory for your project, then navigate into it.
mkdir fastify-messagebird-bulk cd fastify-messagebird-bulkInitialize the Node.js project using npm (or yarn):
This creates a
package.jsonfile.1.2 Install Dependencies
Install Fastify, the MessageBird SDK, BullMQ, Redis client, dotenv, and Prisma (if using database tracking).
# Core dependencies npm install fastify messagebird bullmq ioredis dotenv # Optional: Prisma for database tracking npm install prisma @prisma/client pg --save-dev # Development dependencies (optional but recommended) npm install -D nodemon concurrentlyfastify: The web frameworkmessagebird: The official MessageBird Node.js SDK (formerly referenced as@messagebird/apiin some examples)bullmq: The job queue libraryioredis: A robust Redis client needed by BullMQdotenv: Loads environment variables from a.envfileprisma,@prisma/client,pg: For database interaction (ORM, client, PostgreSQL driver)nodemon: Automatically restarts the server during development on file changesconcurrently: Runs multiple commands concurrently (useful for running the API and worker)1.3 Project Structure
Create the following directory structure within your project root:
mkdir -p src/{config,jobs,routes,services,utils}Your project structure:
1.4 Configure Environment Variables
Create a
.envfile in the project root. This file holds your sensitive credentials and configuration. Never commit this file to version control.Also create a
.env.examplefile to show the required variables..env.example:Copy
.env.exampleto.envand fill in your actual credentials:cp .env.example .envMESSAGEBIRD_API_KEY: Your live API access key from the MessageBird dashboard (Developers → API access → Show key)MESSAGEBIRD_ORIGINATOR: The sender ID for your SMS messages. This can be:+12025550147)MyCompany, max 11 characters). Note: Alphanumeric IDs are not supported in all countries (e.g., the US) and cannot receive replies. Check MessageBird's country restrictions. Use a number for broader compatibilityREDIS_HOST,REDIS_PORT,REDIS_PASSWORD/REDIS_URL: Connection details for your Redis serverDATABASE_URL: (Optional) Connection string for your PostgreSQL database if using Prisma. Follow the format specified in the Prisma documentation for your specific databaseAPI_PORT,API_HOST: Network configuration for the Fastify serverQUEUE_NAME: A name for the BullMQ job queue1.5 Configure
.gitignoreCreate a
.gitignorefile in the root to prevent sensitive files and unnecessary folders from being committed:.gitignore:1.6 (Optional) Initialize Prisma
If you're using the database for tracking, initialize Prisma:
This creates the
prisma/directory and aschema.prismafile.Example Prisma Schema for Message Tracking:
Update
prisma/schema.prisma:Run the migration:
1.7 Configure
package.jsonScriptsAdd scripts to your
package.jsonfor easier development and execution:package.json(add/update thescriptssection):{ "scripts": { "start:api": "node src/api.js", "start:worker": "node src/worker.js", "dev:api": "nodemon src/api.js", "dev:worker": "nodemon src/worker.js", "dev": "concurrently \"npm:dev:api\" \"npm:dev:worker\"", "db:migrate": "npx prisma migrate dev --name init", "db:studio": "npx prisma studio" } }start:api/start:worker: Run the API server and worker in productiondev:api/dev:worker: Run the API server and worker usingnodemonfor development (auto-restarts on changes)dev: Runs both the API and worker concurrently for development usingconcurrentlydb:migrate,db:studio: (Optional) Prisma commands for database migrations and GUIYour basic project structure and configuration are now complete.
2. Implementing MessageBird API Integration and Job Queue
Implement the core logic for sending messages via MessageBird and set up the asynchronous queue.
2.1 Configure MessageBird Service for SMS Sending
Create a service file to encapsulate MessageBird interactions.
src/services/messagebirdService.js:const messagebird = require('messagebird'); const dotenv = require('dotenv'); dotenv.config(); // Load .env variables const apiKey = process.env.MESSAGEBIRD_API_KEY; const originator = process.env.MESSAGEBIRD_ORIGINATOR; if (!apiKey) { console.error('ERROR: MESSAGEBIRD_API_KEY environment variable not set.'); process.exit(1); // Exit if key is missing } if (!originator) { console.warn('WARN: MESSAGEBIRD_ORIGINATOR environment variable not set. Using default or MessageBird assigned number might occur.'); } let messagebirdClient; try { // Initialize the MessageBird client with your API key messagebirdClient = messagebird(apiKey); console.log('MessageBird SDK initialized successfully.'); } catch (error) { console.error('ERROR: Failed to initialize MessageBird SDK:', error); process.exit(1); } /** * Sends a single SMS message using the MessageBird API. * * @param {string} recipient - The recipient's phone number (E.164 format, e.g., +12025550147). * @param {string} body - The text message content. * @returns {Promise<object>} - A promise that resolves with the MessageBird API response. * @throws {Error} - Throws an error if the API call fails. */ const sendSms = async (recipient, body) => { if (!messagebirdClient) { throw new Error('MessageBird SDK is not initialized.'); } if (!recipient || !body) { throw new Error('Recipient and body are required for sending SMS.'); } const params = { originator: originator, recipients: [recipient], body: body, }; console.log(`Attempting to send SMS to ${recipient} via MessageBird…`); // Using Promise wrapper for the callback-based API return new Promise((resolve, reject) => { messagebirdClient.messages.create(params, (err, response) => { if (err) { console.error(`ERROR sending SMS to ${recipient}:`, JSON.stringify(err, null, 2)); reject({ message: `Failed to send SMS via MessageBird to ${recipient}`, details: err.errors || [err] }); } else { console.log(`SUCCESS: MessageBird API response for ${recipient}:`, JSON.stringify(response, null, 2)); resolve(response); } }); }); }; module.exports = { sendSms, messagebirdClient // Export client if needed elsewhere (e.g., for balance checks) };messagebirdpackage. Includes error handling for missing keys or initialization failuressendSmsfunction:recipientandbodyas argumentsparamsobject required bymessagebirdClient.messages.createPromiseto handle the callback-based MessageBird SDK APINote on Bulk Sending: For sending to multiple recipients efficiently, MessageBird also provides a Batch API that allows sending different messages to up to 50 recipients per request. This can be more efficient than individual calls for very large batches.
2.2 Configure Redis and BullMQ for Asynchronous SMS Processing
Set up the connection to Redis and define the BullMQ queue.
src/config/redis.js:const Redis = require('ioredis'); const dotenv = require('dotenv'); dotenv.config(); const redisConfig = { host: process.env.REDIS_HOST || '127.0.0.1', port: process.env.REDIS_PORT || 6379, // password: process.env.REDIS_PASSWORD, // Uncomment if needed maxRetriesPerRequest: null, // Recommended by BullMQ docs }; // Support for Redis URL connection string const redisUrl = process.env.REDIS_URL; let connection; if (redisUrl) { connection = new Redis(redisUrl, { maxRetriesPerRequest: null }); console.log(`Connecting to Redis using URL: ${redisUrl.split('@')[1] || redisUrl}`); // Avoid logging password } else { connection = new Redis(redisConfig); console.log(`Connecting to Redis at ${redisConfig.host}:${redisConfig.port}`); } connection.on('connect', () => { console.log('Successfully connected to Redis.'); }); connection.on('error', (err) => { console.error('Redis connection error:', err); }); module.exports = connection;.envREDIS_URLsrc/jobs/smsQueue.js:const { Queue } = require('bullmq'); const redisConnection = require('../config/redis'); const dotenv = require('dotenv'); dotenv.config(); const QUEUE_NAME = process.env.QUEUE_NAME || 'sms_sending_queue'; // Create a new Queue instance // Reuse the connection object const smsQueue = new Queue(QUEUE_NAME, { connection: redisConnection, defaultJobOptions: { attempts: 3, // Number of times to retry a failed job backoff: { type: 'exponential', // Exponential backoff strategy delay: 1000, // Initial delay in ms (1 second) }, removeOnComplete: true, // Automatically remove job after completion removeOnFail: false, // Keep failed jobs for inspection }, }); console.log(`BullMQ queue "${QUEUE_NAME}" initialized.`); // Optional: Listen for queue events globally if needed (e.g., for monitoring) smsQueue.on('error', (error) => { console.error(`Queue "${QUEUE_NAME}" error:`, error); }); smsQueue.on('waiting', (jobId) => { // console.log(`Job ${jobId} is waiting in queue "${QUEUE_NAME}".`); }); smsQueue.on('active', (job) => { // console.log(`Job ${job.id} has started processing in queue "${QUEUE_NAME}".`); }); smsQueue.on('completed', (job, result) => { // console.log(`Job ${job.id} completed successfully in queue "${QUEUE_NAME}". Result:`, result); }); smsQueue.on('failed', (job, err) => { console.error(`Job ${job.id} failed in queue "${QUEUE_NAME}". Attempt ${job.attemptsMade}/${job.opts.attempts}. Error:`, err); }); /** * Adds a job to send a single SMS message to the queue. * @param {string} recipient - The recipient's phone number. * @param {string} body - The message content. * @returns {Promise<import('bullmq').Job>} - A promise that resolves with the added job. */ const addSmsJob = async (recipient, body) => { if (!recipient || !body) { throw new Error('Recipient and body are required to add SMS job.'); } console.log(`Adding SMS job to queue for recipient: ${recipient}`); const jobData = { recipient, body }; const jobName = 'send-single-sms'; try { const job = await smsQueue.add(jobName, jobData); console.log(`Job ${job.id} added to queue "${QUEUE_NAME}" for recipient ${recipient}.`); return job; } catch (error) { console.error(`Error adding job to queue "${QUEUE_NAME}":`, error); throw error; } }; module.exports = { smsQueue, addSmsJob, QUEUE_NAME };Queueinstance, connecting it to Redis via the shared connectiondefaultJobOptions: Configures crucial settings:attempts,backoff,removeOnComplete/removeOnFailaddSmsJobfunction: Adds a new job to the queue with recipient and body data2.3 Create the Background Worker for SMS Job Processing
This process listens to the queue and executes the jobs.
src/worker.js:const { Worker } = require('bullmq'); const redisConnection = require('./config/redis'); const { QUEUE_NAME } = require('./jobs/smsQueue'); const { sendSms } = require('./services/messagebirdService'); // Optional: Prisma client for database updates // const { PrismaClient } = require('@prisma/client'); // const prisma = new PrismaClient(); console.log(`Starting worker for queue "${QUEUE_NAME}"…`); // Define the processing function for jobs const processSmsJob = async (job) => { const { recipient, body } = job.data; console.log(`Processing job ${job.id} (Attempt ${job.attemptsMade + 1}/${job.opts.attempts}): Send SMS to ${recipient}`); try { // Optional: Update database status to 'processing' before sending // await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'processing' } }); const result = await sendSms(recipient, body); // Optional: Update database status to 'sent' on success // await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'sent', messageBirdId: result.id } }); console.log(`Job ${job.id} completed successfully for ${recipient}. MessageBird Response ID: ${result?.id || 'N/A'}`); return { messageBirdId: result?.id, status: 'sent' }; } catch (error) { console.error(`Job ${job.id} failed for recipient ${recipient}:`, error.message || error); // Optional: Update database status to 'failed' // await prisma.messageLog.update({ // where: { jobId: job.id }, // data: { status: 'failed', failureReason: JSON.stringify(error.details || error.message) } // }); // IMPORTANT: Throw the error again to signal BullMQ that the job failed // This allows BullMQ to handle retries based on the queue's configuration. throw error; } }; // Create a new Worker instance const worker = new Worker(QUEUE_NAME, processSmsJob, { connection: redisConnection, concurrency: 5, // Process up to 5 jobs concurrently limiter: { // Optional: Rate limiting to avoid hitting MessageBird limits max: 10, // Max 10 jobs duration: 1000 // per 1000 ms (1 second) } }); // --- Worker Event Listeners --- worker.on('completed', (job, returnValue) => { // console.log(`Worker completed job ${job.id} for queue "${QUEUE_NAME}". Return value:`, returnValue); }); worker.on('failed', (job, err) => { console.error(`Worker failed job ${job.id} for queue "${QUEUE_NAME}" after ${job.attemptsMade} attempts. Error:`, err.message || err); }); worker.on('error', (err) => { console.error(`Worker error for queue "${QUEUE_NAME}":`, err); }); worker.on('active', (job) => { // console.log(`Worker started processing job ${job.id} for queue "${QUEUE_NAME}".`); }); worker.on('progress', (job, progress) => { // If your job reports progress // console.log(`Worker progress for job ${job.id}:`, progress); }); console.log(`Worker listening for jobs on queue "${QUEUE_NAME}" with concurrency ${worker.opts.concurrency}.`); // Graceful shutdown const gracefulShutdown = async (signal) => { console.log(`\nReceived ${signal}. Closing worker and Redis connection…`); await worker.close(); await redisConnection.quit(); // Optional: Close database connection // await prisma.$disconnect(); console.log('Worker and connections closed gracefully.'); process.exit(0); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Catches Ctrl+CWorkerinstance listening to the queueprocessSmsJobFunction: The core job execution logic, callingsendSms. Handles errors and re-throws them for BullMQ retries. Includes optional Prisma integration points3. Building the Fastify REST API for Bulk SMS
Create the Fastify server and the API endpoint to receive bulk SMS requests.
src/api.js:const Fastify = require('fastify'); const dotenv = require('dotenv'); const { addSmsJob, QUEUE_NAME } = require('./jobs/smsQueue'); // Optional: Prisma client for database interaction // const { PrismaClient } = require('@prisma/client'); // const prisma = new PrismaClient(); dotenv.config(); const server = Fastify({ logger: true // Enable built-in Pino logger }); // --- Request Validation Schema --- const bulkSmsSchema = { body: { type: 'object', required: ['recipients', 'body'], properties: { recipients: { type: 'array', minItems: 1, items: { type: 'string', pattern: '^\\+[1-9]\\d{1,14}$' }, description: 'An array of recipient phone numbers in E.164 format (e.g., +12025550147).' }, body: { type: 'string', minLength: 1, maxLength: 1600, description: 'The text content of the SMS message.' }, correlationId: { type: 'string', description: 'Optional identifier to correlate this batch request.' } }, additionalProperties: false }, response: { 202: { type: 'object', properties: { message: { type: 'string' }, jobCount: { type: 'integer' }, correlationId: { type: 'string', nullable: true }, queueName: { type: 'string' } } } } }; // --- API Route --- server.post('/send-bulk', { schema: bulkSmsSchema }, async (request, reply) => { const { recipients, body, correlationId } = request.body; const addedJobs = []; const failedAdds = []; request.log.info(`Received bulk SMS request. CorrelationID: ${correlationId || 'N/A'}, Recipients: ${recipients.length}, Body: "${body.substring(0, 50)}…"`); try { // Add a job to the queue for each recipient for (const recipient of recipients) { try { // Optional: Create initial DB record before adding job // const dbRecord = await prisma.messageLog.create({ // data: { // recipient: recipient, // body: body, // status: 'pending', // correlationId: correlationId, // } // }); const job = await addSmsJob(recipient, body); addedJobs.push({ recipient: recipient, jobId: job.id }); // Optional: Update DB record with the jobId // await prisma.messageLog.update({ // where: { id: dbRecord.id }, // data: { jobId: job.id } // }); } catch (queueError) { request.log.error(`Failed to add job for recipient ${recipient}:`, queueError); failedAdds.push({ recipient: recipient, error: queueError.message }); } } if (failedAdds.length > 0) { request.log.warn(`Partial success: ${addedJobs.length} jobs added, ${failedAdds.length} failed to queue.`); } reply.code(202).send({ message: `Accepted ${addedJobs.length} SMS messages for background processing.`, jobCount: addedJobs.length, correlationId: correlationId || null, queueName: QUEUE_NAME }); } catch (error) { request.log.error('Error processing bulk SMS request:', error); reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to process the bulk SMS request.' }); } }); // --- Health Check Route --- server.get('/health', async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() }; }); // --- Start Server --- const start = async () => { try { const port = process.env.API_PORT || 3000; const host = process.env.API_HOST || '0.0.0.0'; await server.listen({ port: port, host: host }); server.log.info(`API server listening on ${server.server.address().address}:${server.server.address().port}`); } catch (err) { server.log.error(err); // Optional: Disconnect Prisma on error // await prisma.$disconnect(); process.exit(1); } }; start();bulkSmsSchema): Defines request body structure, including E.164 regex. Defines202 Acceptedresponse schema/send-bulkRoute: Validates input, iterates recipients, callsaddSmsJobfor each, handles queuing errors, returns202 Accepted. Includes optional Prisma integration points/healthRoute: Basic health check endpoint4. MessageBird API Integration Guide
The core integration with MessageBird happens within the
src/services/messagebirdService.jsfile, which was created in Section 2.1.Key Integration Points:
messagebird(process.env.MESSAGEBIRD_API_KEY)securely initializes the SDK using the API key from environment variablesmessagebirdClient.messages.create(params, callback)is the method used to send individual SMS messages. ThesendSmsfunction wraps this logicMESSAGEBIRD_ORIGINATORenvironment variable determines the sender ID.envfileObtaining API Credentials:
MESSAGEBIRD_API_KEYin your.envfileEnvironment Variable Summary:
MESSAGEBIRD_API_KEYlive_xxxxx...)MESSAGEBIRD_ORIGINATOR+12025550147) or alphanumeric (max 11 chars, e.g.,MyCompany)Alternative for Large Volumes: For larger volumes (50+ recipients with same message), consider using the MessageBird Batch API which allows up to 50 recipients per request for improved efficiency.
5. Running Your Bulk SMS Application
Start your bulk SMS system using the npm scripts configured earlier.
5.1 Development Mode
Run both the API server and worker concurrently with auto-restart on file changes:
Or run them separately in different terminals:
# Terminal 1: API Server npm run dev:api # Terminal 2: Background Worker npm run dev:worker5.2 Production Mode
Run the API and worker as separate processes:
# Terminal 1 or Process Manager: API Server npm run start:api # Terminal 2 or Process Manager: Background Worker npm run start:workerProcess Manager Recommendation: Use a process manager like PM2 for production deployments:
# Install PM2 globally npm install -g pm2 # Start both processes pm2 start src/api.js --name "sms-api" pm2 start src/worker.js --name "sms-worker" # View logs pm2 logs # Save process list for restart pm2 save pm2 startup5.3 Testing the Bulk SMS API
Send a test bulk SMS request using curl:
curl -X POST http://localhost:3000/send-bulk \ -H "Content-Type: application/json" \ -d '{ "recipients": ["+12025550147", "+12025550148"], "body": "Hello from MessageBird bulk SMS!", "correlationId": "test-batch-001" }'Expected response (202 Accepted):
{ "message": "Accepted 2 SMS messages for background processing.", "jobCount": 2, "correlationId": "test-batch-001", "queueName": "sms_sending_queue" }Monitor the worker logs to see job processing in real-time.
6. Next Steps and Best Practices
6.1 Security Best Practices
/send-bulkendpoint@fastify/rate-limitto prevent abuse.envfiles. Use secret management services in production (e.g., AWS Secrets Manager, HashiCorp Vault)6.2 Monitoring and Observability
6.3 Performance Optimization
concurrencysetting in your worker based on your server resources and MessageBird rate limits6.4 Error Handling and Recovery
6.5 Cost Optimization
Conclusion
You now have a production-ready bulk SMS broadcast system using MessageBird, Node.js, Fastify, and BullMQ. This architecture handles large message volumes efficiently through asynchronous processing, includes retry logic for reliability, and provides a foundation for scaling.
The key architectural decisions – using BullMQ for job queuing, Redis for persistence, and Fastify for high-performance API handling – create a robust system that can grow with your needs.
Related Resources:
Additional Documentation: