Frequently Asked Questions
Create a Fastify Node.js application with a /broadcast endpoint that uses the Twilio Programmable Messaging API and a Messaging Service. This endpoint receives the message body and recipient numbers, then asynchronously sends the message via Twilio.
A Twilio Messaging Service is a tool that simplifies sending bulk SMS. It manages a pool of sender numbers, handles opt-outs, and improves deliverability and scalability by distributing message volume.
Fastify is a high-performance Node.js framework known for its speed and efficiency, making it well-suited for handling a large volume of API requests in a bulk messaging application.
Use a job queue like BullMQ or RabbitMQ in production environments. The asynchronous processing method with setImmediate shown in the article is not robust enough for high volumes and lacks retry mechanisms essential for reliability.
Yes, Twilio supports international messaging. Ensure your Twilio account has the necessary geo-permissions enabled for the target countries and be aware of international messaging regulations and costs.
Twilio Messaging Services automatically manage standard opt-out keywords (STOP, UNSUBSCRIBE, etc.). No custom logic is required, but inform users how to opt out.
E.164 is an international standard phone number format required by Twilio. It starts with a '+' followed by the country code and national number, for example, +15551234567. The article enforces this with a regular expression.
The provided API key authentication is insufficient for production. Implement stronger methods like OAuth 2.0 or JWT and use a robust secrets management system.
The code uses a Set to remove duplicate recipient numbers before sending messages, ensuring each number receives the message only once per request and avoiding extra costs.
Asynchronous processing with a job queue is crucial. Use a Twilio Messaging Service to scale sending. Keep request payloads and recipient lists within reasonable limits to manage memory usage.
Use Pino for structured logging. Track API requests, errors, job queue metrics, and Twilio message statuses. Forward logs to a central system and configure alerts for key performance indicators.
In the Twilio Console, go to Messaging > Services, create a new service, add your Twilio phone number to its Sender Pool, and copy the Messaging Service SID.
The article suggests a schema including tables for broadcast jobs and individual recipient statuses. This helps track message delivery, errors, and overall job progress. An ORM like Prisma can be helpful for implementation.
Use curl or Postman to send POST requests to the /broadcast endpoint with a JSON body containing the message and recipients. Verify responses and check server and Twilio logs for message status.
Build a Bulk SMS API with Twilio & Fastify: Complete Node.js Guide
Send SMS messages in bulk to reach your audience effectively. Whether you're building promotional campaigns, critical alerts, or notifications, delivering messages reliably and at scale is crucial. This guide demonstrates production-scale capabilities – sending 1,000 to 100,000 messages per hour – using Twilio Messaging Services with proper sender pools and queue-based processing.
This guide walks you through building a scalable bulk messaging API using Fastify – a high-performance Node.js web framework – and Twilio's robust Programmable Messaging API. You'll use Twilio Messaging Services for scalability and deliverability, building a solid foundation for handling large message volumes efficiently. While we aim for robustness, note that certain components (like the basic authentication and asynchronous processing method) serve as starting points and require enhancement for high-volume, mission-critical production environments.
Project Overview and Goals
What You'll Build:
You'll create a Node.js application using the Fastify framework. This application exposes a single API endpoint (
POST /broadcast) that accepts a message body and a list of recipient phone numbers. When you send a request, the API leverages Twilio's Programmable Messaging API via a Messaging Service to initiate sending the specified message to all unique recipients. Expected throughput: 10–100 messages per second with proper configuration, handling 36,000–360,000 messages per hour.Problem This Solves:
You need to send the same SMS message to numerous recipients programmatically without overwhelming the Twilio API or facing deliverability issues from sending high volumes from a single phone number. This solution provides a scalable and reliable method for bulk SMS communication. The approach handles moderate volumes (1,000–10,000 messages per batch) in the demo configuration and scales to enterprise levels (100,000+ per hour) with production enhancements like job queues and expanded sender pools.
Technologies Used:
twilio(Node.js Helper Library): Simplifies interaction with the Twilio API.dotenv: Manages environment variables for secure configuration.pino&pino-pretty: Efficient JSON logging for Node.js, integrated with Fastify.fastify-env: Schema-based environment variable loading and validation for Fastify.@fastify/rate-limit: Adds rate limiting capabilities to the API.Dependency Versions (January 2025):
fastifytwiliodotenvpinopino-prettyfastify-env@fastify/rate-limitSystem Architecture:
(Note: Ensure the Mermaid diagram renders correctly on your publishing platform.)
Production Architecture Note: For production systems handling thousands of messages, replace the simple async processing with a job queue (BullMQ + Redis, RabbitMQ, or AWS SQS). This ensures reliable processing, automatic retries, and horizontal scalability.
Prerequisites:
curlor a tool like Postman for testing the API endpoint.Estimated Costs: Testing with 100–500 messages costs approximately $5–$25 USD (depending on destination countries). Production at 100,000 messages/day costs roughly $600–$800 USD per month for US domestic traffic at $0.0079 per SMS segment. Use Twilio's Pricing Calculator for your specific regions.
Final Outcome:
By the end of this guide, you'll have a functional Fastify API capable of receiving bulk messaging requests and efficiently initiating their delivery via Twilio. Your API will include basic security, validation, logging, and rate limiting – providing a strong starting point for a bulk messaging solution.
1. Setting Up the Project
Initialize your project, install dependencies, and set up the basic structure.
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
Initialize Node.js Project: Initialize the project using npm (or yarn).
Install Dependencies: Install Fastify, the Twilio helper library, logging tools, and environment variable management.
fastify: The core web framework.twilio: Official Node.js library for the Twilio API.dotenv: Loads environment variables from a.envfile.pino-pretty: Formats Pino logs for better readability during development.fastify-env: Validates and loads environment variables based on a schema.@fastify/rate-limit: Plugin for request rate limiting. Source: Fastify Rate Limit (2025)Create Project Structure: Organize the project files for better maintainability.
server.js: The main entry point for the Fastify application..env: Stores sensitive credentials and configuration (API keys, etc.). Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore (like.envandnode_modules).src/routes/broadcast.js: Defines the API route for sending bulk messages.src/config/environment.js: Defines the schema for environment variables usingfastify-env.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them.Set up Environment Variables (
.env): You need three key pieces of information from your Twilio account: Account SID, Auth Token, and a Messaging Service SID.Get Account SID and Auth Token:
Account SIDandAuth Token. Copy these values..envfile and restart your application.Create and Configure a Messaging Service:
Messaging Service SID(it starts withMG...).Populate
.env: Open the.envfile and add your credentials. Replace the placeholders with your actual values. Generate a secure API key using:node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"(minimum 32 bytes of entropy).Define Environment Schema (
src/config/environment.js): Usingfastify-env, define a schema to validate and load environment variables.fastify-env? It ensures required variables are present and validates their format upon application startup, preventing runtime errors due to missing or incorrect configuration. It also conveniently decorates the Fastify instance (fastify.config) with the loaded variables.Basic Fastify Server Setup (
server.js): Configure the main server file to load environment variables, set up logging, register plugins, and define routes.fastify-envensures configuration is loaded and validated before the server starts listening. The authentication hook provides basic protection but includes a warning about its limitations for production.Add Start Script to
package.json: Make it easy to run the server. Add scripts for development, production, and process management.You can now start the server in development using
npm run dev. For production, install PM2 globally (npm install -g pm2) and usenpm run pm2:startfor process management with automatic restarts.2. Implementing Core Functionality & API Layer
Now, build the
/broadcastendpoint that handles sending messages.Define the Broadcast Route (
src/routes/broadcast.js): This file contains the logic for the/api/v1/broadcastendpoint.messageis a non-empty string andrecipientsis an array of strings matching the E.164 format.[...new Set(recipients)]to ensure each phone number is processed only once per request.202 Acceptedresponse.setImmediateschedules the sending logic to run asynchronously.setImmediateis not suitable for production scale. It lacks error handling resilience (like retries) and can lead to memory issues under heavy load. A background job queue is essential for reliable production systems.Promise.allSettled: Iterate over unique recipients and attempt to send a message to each, allowing processing to continue even if some attempts fail initially.error.codeusingerror?.code.3. Testing the API Layer
Use
curlor Postman to test the/api/v1/broadcastendpoint.Start the Server:
Send a Test Request (
curl): Replace<YOUR_NUMBER_1>and<YOUR_NUMBER_2>with valid phone numbers in E.164 format (e.g.,+15551234567). Replace<YOUR_API_SECRET_KEY>with the value from your.envfile.Expected Response (JSON): You should receive an immediate
202 Acceptedresponse. NoticerecipientsCountreflects the unique count.Check Server Logs: Observe the terminal where
npm run devis running. You should see logs indicating:Check Twilio Logs: Log in to the Twilio Console and navigate to Monitor > Logs > Messaging. You should see the outgoing messages initiated by your application via the Messaging Service (one for each unique recipient). Message statuses progress through:
queued→sent→delivered(orfailed/undeliveredif issues occur).4. Integrating with Twilio (Recap)
You've already set up the core integration. Here are the crucial Twilio elements:
.env): Your main account credentials authenticate thetwilioclient. Obtain these from the Twilio Console dashboard..env):Messaging Service SID(starting withMG...) is used in the API call instead of a specificfromnumber.5. Error Handling, Logging, and Retry Mechanisms
Error Handling Strategy:
onRequesthook rejects unauthorized requests.try...catchblock within themapfunction insidesetImmediatecatches errors during individualtwilioClient.messages.createcalls.Promise.allSettledensures one failure doesn't stop others.log.error, including the recipient number, error message, and Twilio error code (if available, usingerror?.code).Logging:
pinofor structured JSON logging (in production) or human-readable output (pino-prettyin development).error.message:* AND errorCode:*recipient:"+15551234567"errorCode:* | stats count by errorCodeRetry Mechanisms:
setImmediatecritically lacks any built-in mechanism to retry failed Twilio API calls (e.g., due to temporary network issues connecting to Twilio). Errors are logged, but attempts are not automatically retried.setImmediatewith a dedicated job queue system. Job queues (like BullMQ, RabbitMQ) typically provide built-in, configurable retry mechanisms (e.g., exponential backoff). Example retry configuration with BullMQ:Avoid retrying non-recoverable errors like invalid phone numbers (Twilio error code
21211). This is a crucial step for production readiness.Common Twilio Error Codes to Handle:
When implementing retry logic, distinguish between retryable and non-retryable errors:
Non-Retryable Errors (log and skip):
Retryable Errors (implement exponential backoff):
Account/Permission Errors (requires manual intervention):
Source: Twilio Error Codes (2025)
6. Database Schema and Data Layer (Conceptual)
While this guide focuses on the core API, a production system often requires persistence for tracking and auditing.
Why a Database?
Conceptual Schema (using Prisma syntax as an example):
Implementation:
BroadcastJobrecord in the database withstatus: 'processing'.setImmediateblock or, preferably, a job queue worker), update the correspondingRecipientStatusrecord for each outcome (success/failure, SID, error).BroadcastJobrecord withstatus: 'completed'and the final counts./webhooks/statusendpoint to receive Twilio delivery status callbacks, updatingRecipientStatusrecords with final delivery confirmation.This guide omits DB integration for brevity, focusing purely on the Fastify/Twilio interaction.
7. Security Features
Input Validation: Handled by Fastify's schema validation in the route definition. Ensures
messageexists andrecipientsare correctly formatted (E.164 strings) and within size limits. Prevents basic injection risks and ensures data integrity.Authentication: Implemented using a simple API Key check (
X-API-Keyheader) in theonRequesthook.@fastify/oauth2plugin. For JWT, use@fastify/jwtplugin:Store secrets in AWS Secrets Manager, HashiCorp Vault, or Azure Key Vault for production.
Authorization: Currently, any valid API key grants full access. Implement role-based access control (RBAC) if different users/systems require varying permissions (e.g., only admins can broadcast).
Rate Limiting: Implemented using
fastify-rate-limit.maxandtimeWindowoptions inserver.jscontrol the limit. Adjust these based on expected usage and capacity.keyGeneratoroption:Secrets Management: API keys and Twilio credentials are stored in
.envand loaded securely. Ensure.envis never committed to Git. Use platform-specific secret management tools (like AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) in production environments.Common Vulnerabilities: Keep dependencies updated (
npm audit fix) to patch known vulnerabilities. The schema validation provides a layer of defense against injection attacks targeting the expected data structure.8. Handling Special Cases
E.164 Phone Number Format: Enforced via regex in the schema (
^\+[1-9]\d{1,14}$). This is Twilio's required format. Ensure client applications sending requests adhere to this. For robust phone number validation and normalization, use thelibphonenumber-jslibrary:SMS Character Limits & Segmentation: A single SMS segment has specific limits based on encoding:
Twilio automatically handles segmentation for longer messages, sending them as multiple parts that are reassembled on the recipient's handset. Twilio recommends keeping messages under 320 characters for best deliverability, and supports messages up to 1,600 characters across messaging channels. Be mindful that sending multi-segment messages costs more (charged per segment). The schema limits the message length to 1,600 characters (approximately 10 segments) as a practical upper bound. Use Twilio's Message Segment Calculator to understand encoding and segment impacts for frequently sent messages.
Cost Examples:
Source: Twilio SMS Character Limit (2025)
Opt-Out Handling: Twilio Messaging Services handle standard English opt-out keywords (STOP, UNSUBSCRIBE, CANCEL, END, QUIT) automatically. When a user replies with one of these, Twilio prevents further messages from that specific Messaging Service to that user. You don't need to implement this logic yourself, but you should inform users of how to opt out. You can configure custom opt-out keywords and responses within the Messaging Service settings in the Twilio Console. Database Sync: Configure Twilio webhooks at Messaging > Services > [Your Service] > Integration to send opt-out events to your
/webhooks/optoutendpoint. Update your user database to mark opted-out recipients and filter them from future broadcasts.Duplicate Recipients: The code now handles duplicate recipients by using
[...new Set(recipients)]insrc/routes/broadcast.jsbefore initiating the sending process. This ensures efficiency and avoids unnecessary costs.International Messaging: Twilio supports global messaging, but ensure your account has permissions enabled for the countries you intend to send to (check Console > Messaging > Settings > Geo-permissions). Be aware of country-specific regulations:
Cost differences vary significantly (e.g., US: $0.0079, UK: $0.0582, India: $0.0073 per segment). Source: Twilio International Messaging (2025)
9. Performance Optimizations
Asynchronous Processing: Using a background job queue (instead of the demo
setImmediate) is the most critical performance and reliability optimization for handling bulk sends without blocking the API. Queue Comparison:Twilio Messaging Service: Using a Messaging Service is crucial for scaling throughput beyond the 1 message per second limit of a single standard Twilio number. Twilio distributes the load across the numbers in the service pool. Scaling Thresholds:
Add more numbers to the pool as volume increases. Monitor queue depth and error rates to determine when to scale.
Payload Size: Keep the request payload reasonable. The schema limits recipients to 1,000 per request – adjust based on testing, typical batch sizes, and memory constraints. Very large arrays increase memory usage during initial processing. Consider batching large lists on the client-side if necessary.
Twilio Client Initialization: The
twilioClientis initialized once when the route is set up, avoiding redundant object creation per request.Load Testing: Use tools like
k6(k6.io) orautocannon(npm install -g autocannon) to simulate concurrent requests and identify bottlenecks in your API, background processing, or infrastructure.Expected Baseline Performance: A well-configured Fastify API on 2 CPU cores should handle 500–1,000 requests/second for the
/broadcastendpoint. Actual message throughput depends on Twilio account limits and sender pool size.Node.js Performance: Ensure you are using an LTS version of Node.js. Profile your application using Node's built-in profiler (
node --prof) or tools like Clinic.js (npm install -g clinic) if you suspect CPU or memory bottlenecks, especially within the background processing logic.10. Monitoring, Observability, and Analytics
Health Checks: The
/healthendpoint provides a basic check that the server is running and responsive. Monitoring tools should poll this endpoint regularly.Logging (Recap): Structured JSON logs are essential. Ensure they capture key information: request IDs, timestamps, recipient counts, individual message SIDs, errors (with codes), and processing duration. Forward logs to a centralized system.
Metrics: Track key performance indicators (KPIs):
/broadcast).fastify-metricscan help expose Prometheus metrics. Example Metrics to Track:http_requests_total{method="POST", route="/broadcast", status="202"}– total broadcasts acceptedtwilio_messages_sent_total{status="success"}– successful Twilio API callstwilio_messages_sent_total{status="error"}– failed Twilio API callsbroadcast_processing_duration_seconds– time to process each batchTwilio Console Monitoring: Regularly check the Twilio Console (Monitor > Logs > Messaging) for message statuses (sent, delivered, undelivered, failed), error codes, and cost insights. Use Twilio Insights for deeper analytics on deliverability and engagement.
Alerting: Set up alerts based on logs and metrics:
Example PagerDuty Alert Configuration:
SLA Targets: Establish service-level agreements for production:
Define incident response procedures: on-call rotation, escalation paths, and post-mortem templates.