Frequently Asked Questions
Build a scalable bulk SMS application using NestJS, Vonage Messages API, and BullMQ. This setup allows you to create a NestJS API endpoint to handle requests, queue messages with BullMQ and Redis, and process them in the background with a worker that interacts with the Vonage API.
BullMQ, a Redis-based message queue, is crucial for handling background job processing, rate limiting, and retries in bulk SMS applications. It decouples the API request from the actual sending, enabling throttling and reliable message delivery even with temporary failures.
A message queue like BullMQ helps manage provider rate limits, ensuring deliverability and graceful failure handling. Without a queue, simply looping through recipients can lead to errors, blocked messages, and an unreliable system. Queues enable asynchronous processing and retries.
Use the Vonage Messages API when you need to send messages across various channels, including SMS, to a large audience. The provided example uses the official @vonage/server-sdk to interact with the API, sending text messages within the configured rate limits.
Yes, the example demonstrates basic status tracking using Prisma and PostgreSQL. Individual messages are initially marked as PENDING, then PROCESSING, and finally SENT or FAILED. Vonage can optionally send Delivery Receipts (DLRs) via webhooks to update the message status further.
The example leverages BullMQ's rate limiting feature. The worker processes a configurable number of jobs within a specific timeframe (e.g., one per second), ensuring compliance with Vonage's limits for different number types like long codes.
Prisma simplifies database access, migrations, and type safety with PostgreSQL. It's a modern database toolkit for TypeScript and Node.js that makes it easier to interact with your database and manage its schema.
The example provides a Dockerfile and docker-compose.yml to containerize the NestJS application, PostgreSQL database, and Redis instance. This ensures a consistent development and deployment environment and simplifies setup.
Prerequisites include Node.js (LTS recommended), npm or yarn, Docker and Docker Compose, a Vonage API account, a Vonage phone number capable of sending SMS, and basic understanding of NestJS, TypeScript, APIs, and databases. You'll also need curl or Postman for testing.
The application demonstrates error handling and automatic retries. BullMQ automatically requeues failed jobs after a backoff period. If the final attempt fails, the message status is updated to FAILED, and error details are logged.
The API receives recipient lists and message content via POST requests to /broadcasts. It then creates database records and adds individual jobs to the BullMQ queue for each recipient, allowing for asynchronous processing.
Redis serves as the backend for BullMQ, storing the queued SMS sending jobs. It's an in-memory data structure store, providing speed and efficiency for the message queue operations.
The Vonage private key is used to authenticate the application with the Vonage Messages API. The file should be kept secure, mounted into the container read-only, and never committed to version control.
Vonage credentials like API key, secret, application ID, sender ID, and private key path are stored in a .env file. This file should never be committed to version control to protect sensitive information.
Build Bulk SMS Broadcasting with NestJS, Vonage Messages API & BullMQ
Build a robust bulk SMS broadcasting system that handles provider rate limits, ensures deliverability, and manages failures gracefully. Looping through recipients and calling an API directly leads to errors, blocked messages, and unreliable delivery.
This guide shows you how to build a scalable bulk SMS application using NestJS, the Vonage Messages API, and BullMQ for background job processing and rate limiting. You'll decouple the initial API request from the sending process, throttle message dispatch according to Vonage's rate limits (30 API requests per second default, carrier-restricted to 1 request/second for certain number types), and automatically retry failed attempts.
<!-- DEPTH: Introduction lacks specific problem statement with concrete failure scenarios (Priority: Medium) --> <!-- GAP: Missing cost implications/pricing considerations for bulk SMS (Type: Substantive) -->
What You'll Learn:
Prerequisites: Basic understanding of NestJS, TypeScript, APIs, databases, Docker, and message queues.
<!-- GAP: Missing specific version/experience level requirements (Type: Critical) --> <!-- DEPTH: Prerequisites too vague - should specify minimum knowledge thresholds (Priority: High) -->
Project Goals:
Technologies Used:
@vonage/server-sdk.<!-- EXPAND: Could benefit from technology comparison table showing why these were chosen over alternatives (Type: Enhancement) -->
System Architecture:
The system follows this general flow:
/broadcasts) with a list of recipients and the message text.<!-- EXPAND: Architecture section would benefit from a visual diagram (Type: Enhancement) --> <!-- DEPTH: Missing discussion of failure scenarios and edge cases in architecture flow (Priority: Medium) -->
Prerequisites:
curlor a tool like Postman for testing the API.<!-- GAP: Missing minimum hardware/system requirements (Type: Substantive) --> <!-- GAP: Missing information on Vonage account setup process and phone number purchase steps (Type: Critical) -->
Final Outcome:
You will have a containerized NestJS application with an API endpoint to initiate bulk SMS broadcasts. Messages will be reliably sent in the background, respecting Vonage rate limits, with status tracking and automatic retries.
1. Setting Up the Project
Initialize the NestJS project, set up the directory structure, install dependencies, and configure Docker.
1.1 Initialize NestJS Project
Open your terminal and run the NestJS CLI command:
<!-- DEPTH: Missing explanation of flags and options for nest new command (Priority: Low) -->
1.2 Project Structure
Organize your code into modules for better separation of concerns:
<!-- EXPAND: Could add brief explanation of each module's responsibility and interaction patterns (Type: Enhancement) -->
1.3 Install Dependencies
Install the necessary packages for BullMQ, Vonage, Prisma, configuration, and validation:
<!-- GAP: Missing package version compatibility matrix or peer dependency warnings (Type: Substantive) --> <!-- DEPTH: No explanation of what each package does and why it's needed (Priority: Medium) -->
1.4 Configure Environment Variables
Create a
.envfile in the project root. This file stores sensitive credentials and configuration. Never commit this file to version control.private.keyfile when you create a Vonage Application for the Messages API. The Sender ID is your Vonage virtual number.docker-compose.yml.<!-- GAP: Missing step-by-step instructions for obtaining Vonage credentials and creating application (Type: Critical) --> <!-- GAP: Missing security best practices for credential management in production (Type: Critical) --> <!-- DEPTH: Insufficient explanation of rate limit values and how to determine optimal settings (Priority: High) -->
1.5 Configure Docker
Create a
Dockerfilein the project root:<!-- DEPTH: Missing explanation of multi-stage build benefits and security considerations (Priority: Medium) --> <!-- GAP: No discussion of image size optimization or layer caching strategies (Type: Enhancement) -->
Create a
docker-compose.ymlfile:.envfile to configure containers.private.keyfile into the application container (ensure this file exists before runningdocker-compose up).depends_onconditions.<!-- GAP: Missing data persistence and backup strategy discussion (Type: Substantive) --> <!-- DEPTH: No explanation of network isolation or security implications of exposed ports (Priority: Medium) -->
Create a
.dockerignorefile to optimize build context:1.6 Initialize Prisma
Run the Prisma init command:
This creates a
prismadirectory with aschema.prismafile and updates.envwith a placeholderDATABASE_URL. Replace the placeholderDATABASE_URLin.envwith the one you defined earlier, matching the Docker Compose setup (quote it if it contains special characters).Modify
prisma/schema.prismain Section 6 when defining the data model.<!-- GAP: Section 6 reference is invalid - no Section 6 exists with Prisma schema definition (Type: Critical) --> <!-- DEPTH: Missing complete Prisma schema definition for Broadcast and Message models (Priority: Critical) -->
1.7 Configure NestJS Modules
Import and configure the modules you'll use.
Update
src/app.module.ts:This root module now:
ConfigModule..env.ThrottlerModule) using Redis for distributed storage.<!-- GAP: Missing PrismaModule implementation code (Type: Critical) --> <!-- DEPTH: No explanation of module initialization order or dependency resolution (Priority: Medium) -->
2. Implementing Core Functionality (Queue & Worker)
Set up BullMQ to handle the SMS jobs and create the worker process that consumes these jobs.
2.1 Configure Queue Module
Create
src/queue/queue.module.ts:BullModule.registerQueueAsync: Registers a specific queue namedbroadcast-sms.defaultJobOptions: Sets default behavior for jobs added to this queue (attempts, backoff strategy).limiter: Crucially, this configures BullMQ's built-in rate limiter. It ensures theBroadcastProcessorprocesses onlyBULLMQ_RATE_LIMIT_MAXjobs withinBULLMQ_RATE_LIMIT_DURATIONmilliseconds, effectively throttling calls to the Vonage API.VonageModule,PrismaModule, andConfigModuleto make their services injectable into theBroadcastProcessor.<!-- DEPTH: Missing discussion of removeOnComplete vs removeOnFail tradeoffs (Priority: Medium) --> <!-- EXPAND: Could add examples of different backoff strategies and when to use each (Type: Enhancement) -->
2.2 Create Vonage Service
This service encapsulates interaction with the Vonage SDK.
Create
src/vonage/vonage.service.ts:Create
src/vonage/vonage.module.ts:.env. Includes path resolution and existence check for the private key.sendSmsmethod that wraps the SDK'smessages.sendcall.message_uuidon success and potential error details on failure.<!-- DEPTH: Missing explanation of different Vonage authentication methods (JWT vs API Key) (Priority: Medium) --> <!-- GAP: No discussion of message length limits and message segmentation (Type: Substantive) --> <!-- GAP: Missing error code mapping and retry strategy guidance (Type: Substantive) -->
2.3 Create the Queue Processor
This class defines how the
broadcast-smsqueue handles jobs.Create
src/queue/broadcast.processor.ts:@Processor('broadcast-sms'): Decorator linking this class to the specified queue.process(job: Job): The core method BullMQ calls for each job. It receives thejobobject containing your data (recipient,messageText,messageId).VonageService,PrismaService, andConfigService.PENDING→PROCESSING→SENT/FAILED). Increments attempt count.VonageServiceand general processing errors.defaultJobOptions. Updates DB status accordingly:FAILEDon final failure, keepsPROCESSINGduring intermediate retry attempts, storing the latest error.@OnWorkerEvent) for detailed logging.<!-- DEPTH: Missing discussion of concurrency settings and how to scale workers (Priority: High) --> <!-- GAP: No explanation of stalled job recovery strategy or timeout configuration (Type: Substantive) --> <!-- GAP: Missing transaction handling for database updates (Type: Substantive) -->
3. Building the API Layer
This layer exposes an endpoint to receive broadcast requests and add jobs to the queue.
3.1 Create DTO (Data Transfer Object)
DTOs define the expected request body structure and enable automatic validation using
class-validator.Create
src/broadcast/dto/create-broadcast.dto.ts:class-validatorto enforce rules (must be an array, not empty, must contain valid phone numbers, message must be a non-empty string).@ApiPropertyis commented out but can be used if you integrate Swagger.<!-- DEPTH: Missing validation for message length limits and character encoding (Priority: High) --> <!-- GAP: No discussion of recipient list size limits or pagination strategy (Type: Substantive) --> <!-- EXPAND: Could add example of custom validators for specialized phone number formats (Type: Enhancement) -->
3.2 Create Broadcast Service
This service handles the business logic: creating the broadcast record and adding individual message jobs to the queue.
Create
src/broadcast/broadcast.service.ts:@InjectQueue('broadcast-sms'): Injects the BullMQ queue instance.PrismaService.createBroadcast:Broadcastrecord in the database.Messagerecord (statusPENDING) in the database for tracking.broadcastQueuewith themessageId(linking back to the DB record), recipient, and message text.Broadcastobject.getBroadcastStatus,getMessageStatus) for querying the state of broadcasts and individual messages from the database.<!-- GAP: Missing BroadcastController implementation (Type: Critical) --> <!-- GAP: Missing BroadcastModule definition (Type: Critical) --> <!-- DEPTH: No discussion of transaction handling for partial broadcast failures (Priority: High) --> <!-- GAP: Missing duplicate detection strategy for repeated broadcasts (Type: Substantive) --> <!-- EXPAND: Could add example of batch insertion optimization for large recipient lists (Type: Enhancement) -->
Frequently Asked Questions About Bulk SMS Broadcasting with NestJS
How do I handle Vonage API rate limits in NestJS?
Use BullMQ's built-in rate limiter with the
limiterconfiguration in your queue setup. Setmax: 1andduration: 1000to process 1 job per second, respecting Vonage's carrier restrictions (1 request/second for certain number types). The default API limit is 30 requests/second, but carrier constraints often reduce this. BullMQ automatically throttles job processing to prevent API throttling errors.<!-- DEPTH: FAQ answer lacks concrete examples of different rate limit configurations (Priority: Medium) -->
What Node.js version should I use for this NestJS SMS application?
Use Node.js 22.x (Active LTS until October 2025) or Node.js 20.x (maintenance mode) for production. The article's Dockerfile uses Node.js 18, but upgrade to Node.js 22 for extended support (maintenance until April 2027). Node.js 18.x reaches end-of-life on April 30, 2025.
How does BullMQ handle failed SMS messages?
BullMQ automatically retries failed jobs based on your
defaultJobOptionsconfiguration. Setattempts: 3for 3 retry attempts andbackoff: { type: 'exponential', delay: 5000 }for exponential backoff (5s, 10s, 20s delays). TheBroadcastProcessortracks attempt counts in the database and marks messages asFAILEDafter exhausting all retries, storing error details for debugging.<!-- DEPTH: Missing examples of different backoff strategies and when to use each (Priority: Medium) -->
What is the E.164 phone number format required by Vonage?
E.164 format is the international standard for phone numbers: starts with
+followed by country code, then subscriber number, maximum 15 digits total. Example: US number(212) 123-1234becomes+12121231234. The format removes spaces, hyphens, and parentheses. Use@IsPhoneNumber(undefined, { each: true })in your DTO to validate E.164 format, or implement google-libphonenumber for robust validation and formatting.<!-- DEPTH: Answer contradicts itself - E.164 requires + prefix but says no + prefix (Priority: High) -->
Do I need 10DLC registration for US SMS?
Yes, 10DLC Brand and Campaign registration is mandatory for sending SMS to US recipients using standard long code numbers. Without registration, carriers will block your messages. 10DLC registration improves deliverability and throughput (up to 60 messages/second for verified campaigns vs. 1 message/second unverified). Register through your Vonage Dashboard.
<!-- GAP: Missing step-by-step 10DLC registration process details (Type: Substantive) --> <!-- GAP: Missing information about registration costs and timeline (Type: Substantive) -->
How do I monitor BullMQ queue performance?
Install Bull Board for visual queue monitoring:
npm install @bull-board/api @bull-board/nestjs @bull-board/express. Bull Board provides a web UI showing active jobs, completed jobs, failed jobs, job delays, and queue throughput. Mount it at/admin/queuesin your NestJS app. Additionally, implement Prometheus metrics with@willsoto/nestjs-prometheusto track job processing rates, failure rates, and queue depth.<!-- GAP: Missing code example for Bull Board integration (Type: Substantive) -->
What database indexes should I create for Prisma SMS tracking?
Create indexes on frequently queried columns:
broadcastId(for fetching all messages in a broadcast),status(for counting SENT/FAILED/PENDING messages), andcreatedAt(for time-based queries). Add@@index([broadcastId, status])composite index in your Prisma schema for efficient broadcast status queries. Indexrecipientif you need to search by phone number or detect duplicates.How do I handle webhook callbacks from Vonage?
Create a dedicated webhook endpoint (e.g.,
/webhooks/vonage/status) to receive Delivery Receipts (DLRs). Parse the webhook payload to extractmessage_uuidand delivery status (delivered,failed,rejected). Update yourMessagerecord status in the database usingproviderMessageId(stores themessage_uuid). Validate webhook signatures using Vonage's signature verification to prevent spoofed requests.<!-- GAP: Missing code example for webhook endpoint implementation (Type: Substantive) --> <!-- GAP: Missing webhook signature verification implementation details (Type: Critical) -->
Can I send MMS or WhatsApp messages with this architecture?
Yes, the Vonage Messages API supports SMS, MMS, WhatsApp, Viber, and Facebook Messenger through a unified interface. Modify the
MessageSendRequestinVonageService.sendSms()to setchannel: 'mms'orchannel: 'whatsapp'. For MMS, addimage: { url: 'https://...' }to the message object. For WhatsApp, ensure you have a verified WhatsApp Business Account and use template messages for initial contact.<!-- EXPAND: Could add code examples for MMS and WhatsApp message sending (Type: Enhancement) -->
How do I scale this application for millions of messages?
Horizontal scaling: Run multiple worker instances (separate containers) connected to the same Redis queue. Each worker processes jobs concurrently while BullMQ's distributed locking prevents duplicate processing. Vertical scaling: Increase BullMQ's
concurrencysetting inWorkerOptionsto process multiple jobs per worker simultaneously. Database scaling: Use PostgreSQL read replicas for status queries, primary for writes. Consider sharding broadcasts by region or time period for very high volumes (100M+ messages/month).<!-- DEPTH: Scaling answer lacks specific concurrency values and performance benchmarks (Priority: Medium) --> <!-- GAP: Missing discussion of Redis clustering for high availability (Type: Substantive) -->
Next Steps for Production-Ready Bulk SMS Broadcasting
Now that you've built your bulk SMS system, enhance it with these production features:
Implement Webhook Handler – Create
/webhooks/vonage/dlrendpoint to receive Delivery Receipts and update message status toDELIVEREDorFAILEDbased on carrier feedback. Verify webhook signatures with Vonage's JWT validation.Add Bull Board Monitoring – Install
@bull-board/nestjsto visualize queue performance, inspect failed jobs, manually retry jobs, and monitor throughput. Essential for debugging production issues.Configure Prometheus Metrics – Integrate
@willsoto/nestjs-prometheusto track custom metrics: messages sent per minute, failure rate by error type, average delivery time, queue depth. Export to Grafana for real-time dashboards.Implement Message Deduplication – Add Redis-based deduplication using
recipient + message contenthash with 24-hour TTL to prevent accidental duplicate sends during retries or API errors.Optimize Database Indexes – Add Prisma indexes on
@@index([broadcastId, status]),@@index([status, createdAt]), and@@index([providerMessageId])for fast status queries and webhook lookups.Add Rate Limit Overrides – Implement per-broadcast rate limit configuration to handle different Vonage number types (short codes: 100 msg/sec, long codes: 1 msg/sec, 10DLC: varies by campaign tier).
Implement Circuit Breaker – Use
@nestjs/circuit-breakerorcockatielto detect Vonage API outages and pause job processing automatically, preventing queue buildup and unnecessary retries during downtime.Configure Job Prioritization – Use BullMQ's
priorityoption to send urgent messages (OTPs, alerts) before marketing broadcasts. Higher priority (lower number) jobs process first.Add Message Template System – Create a Prisma
MessageTemplatemodel with variable substitution (Hello {{name}}, your code is {{code}}) to enable personalized bulk messages without storing full content per recipient.Implement Webhook Retry Logic – Configure
@nestjs/axioswith exponential backoff to retry failed webhook delivery for DLRs, ensuring your status tracking stays accurate even during temporary network issues.<!-- GAP: Missing complete Prisma schema definition referenced throughout the article (Type: Critical) --> <!-- GAP: Missing main.ts configuration and startup logic (Type: Critical) --> <!-- GAP: Missing testing strategy and example tests (Type: Substantive) --> <!-- GAP: Missing deployment instructions and environment-specific configuration (Type: Substantive) -->
Additional Resources: