code examples
code examples
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.
Build a Bulk SMS System with MessageBird, Node.js, and Fastify
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
.envfile
System Architecture:
Here's how the components interact:
+-----------------+ +-----------------+ +-----------------+ +-------------------+ +------------------+
| Client (e.g. UI)| ---> | Fastify API | ---> | Redis (BullMQ) | ---> | Background Worker | ---> | MessageBird API |
| | | (Accepts Request)| | (Job Queue) | | (Processes Jobs) | | (Sends SMS) |
+-----------------+ +-----------------+ +-----------------+ +-------------------+ +------------------+
| | | ^
| | Optional: Write Job Status | | Optional: Read Job Status
| V V |
+----------------------|-------------------------------------------------|---------+
| +-----------------+
+----------------> | Database (PG) |
| (Tracks Messages)|
+-----------------+What You'll Build:
By the end of this guide, you will have:
- 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
Install 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):
npm init -yThis creates a package.json file.
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:
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.json1.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:
# 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_queueCopy .env.example to .env and 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:- 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
- A purchased virtual mobile number (VMN) from MessageBird (e.g.,
REDIS_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 queue
1.5 Configure .gitignore
Create a .gitignore file in the root to prevent sensitive files and unnecessary folders from being committed:
.gitignore:
# 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.db1.6 (Optional) Initialize Prisma
If you're using the database for tracking, initialize Prisma:
npx prisma init --datasource-provider postgresqlThis creates the prisma/ directory and a schema.prisma file.
Example Prisma Schema for Message Tracking:
Update prisma/schema.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:
npm run db:migrate1.7 Configure package.json Scripts
Add scripts to your package.json for easier development and execution:
package.json (add/update the scripts section):
{
"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 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:
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)
};- Initialization: Loads the API key and originator from environment variables and initializes the MessageBird client using the
messagebirdpackage. Includes error handling for missing keys or initialization failures sendSmsfunction:- Takes
recipientandbodyas arguments - Constructs the
paramsobject required bymessagebirdClient.messages.create - Uses a
Promiseto 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
- Takes
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:
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;- 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:
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
};- Initialization: Creates a
Queueinstance, connecting it to Redis via the shared connection defaultJobOptions: Configures crucial settings:attempts,backoff,removeOnComplete/removeOnFail- Queue Events: Includes listeners for various queue events for logging and monitoring
addSmsJobfunction: 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:
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+C- Initialization: Creates a
Workerinstance listening to the queue processSmsJobFunction: The core job execution logic, callingsendSms. 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.
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();- Fastify Instance: Creates server with logging
- Validation Schema (
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 endpoint- 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. ThesendSmsfunction wraps this logic - Configuration:
MESSAGEBIRD_ORIGINATORenvironment 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
.envfile - 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
Obtaining API Credentials:
- Log in to your MessageBird Dashboard
- Navigate to Developers in the left-hand sidebar
- Click on API access
- 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_KEYin your.envfile - 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:
npm run devOr 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
- API Authentication: Add authentication middleware (e.g., API keys, JWT) to protect your
/send-bulkendpoint - Rate Limiting: Implement rate limiting on API endpoints using
@fastify/rate-limitto 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
.envfiles. 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
concurrencysetting 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.
Related Resources:
- Send SMS with Node.js, Fastify & MessageBird: Complete Tutorial
- Build SMS Marketing Campaigns with MessageBird, Node.js & Fastify
- MessageBird OTP/2FA Tutorial with Node.js and Fastify
Additional Documentation:
Frequently Asked Questions
How to send bulk SMS with Node.js and Fastify?
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.
What is the MessageBird Node.js SDK used for?
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.
Why use Redis with BullMQ for bulk SMS?
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.
When should I use Prisma in a bulk SMS app?
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.
How to set up MessageBird API key in Fastify app?
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.
How to handle SMS queuing failures in Fastify?
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.
What is the role of the background worker in bulk SMS sending?
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.
How to configure rate limiting for MessageBird API calls?
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.
What is the recommended Node.js version for this app?
The guide recommends Node.js v18 or later. This ensures compatibility with Fastify, BullMQ, the MessageBird SDK, and other dependencies.
How to choose a MessageBird originator for sending SMS?
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.
What is Fastify and why is it chosen for this project?
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.
How to configure Redis for BullMQ in the Fastify app?
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.
What does the .gitignore file contain?
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.
Can I use a different database with Prisma?
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.
How to run the Fastify API and background worker concurrently during development?
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.