code examples
code examples
How to Send SMS with Twilio in Node.js Express: Step-by-Step Tutorial (2025)
Learn how to send SMS messages using Twilio API in Node.js Express. Complete guide with REST API setup, Twilio SDK integration, authentication, error handling, and production deployment best practices.
Send SMS with Node.js, Express, and Twilio
Time to Complete: 30–45 minutes Skill Level: Intermediate (basic Node.js and REST API knowledge required)
Learn how to send SMS messages programmatically using the Twilio API with Node.js and Express. This step-by-step tutorial shows you how to build a production-ready REST API endpoint that sends text messages using the Twilio Node.js SDK. You'll learn Twilio authentication, SMS delivery tracking, error handling, rate limiting, and security best practices.
Whether you need to send SMS notifications, two-factor authentication codes, or marketing messages, this guide covers everything from basic Twilio setup to production deployment. By the end, you'll have a working Express server that reliably sends SMS messages through Twilio's messaging API.
What You'll Build: Twilio SMS API with Node.js
- Goal: Build a REST API endpoint (
POST /send) using Node.js and Express that sends SMS messages using the Twilio API. - Problem Solved: Provides a backend service for applications needing programmatic SMS sending capabilities.
- Technologies:
- Node.js: JavaScript runtime for building the server-side application (Node.js 18+ recommended as of 2025).
- Express: Minimalist web framework for Node.js, used to create the API endpoint.
- Twilio Node.js SDK: Official library for interacting with Twilio APIs (supports Node.js 14, 16, 18, 20, 22 LTS).
- dotenv: Module to load environment variables from a
.envfile intoprocess.env.
- Outcome: A runnable Node.js Express application with a single endpoint that sends SMS messages via Twilio.
- Prerequisites:
- Node.js 18 or higher and npm (or yarn) installed. Download Node.js.
- A Twilio account with verified phone number (a free trial account is sufficient to start). Sign up for Twilio.
- A text editor or IDE (like VS Code).
- Optional: An API testing tool like Postman or
curl.
System Architecture
The request/response flow follows this pattern:
- A client (e.g., Postman, frontend application, another backend service) sends a POST request to your Express API's
/sendendpoint with the recipient's phone number and the message text. - Express receives the request and validates the input.
- The application uses the Twilio Node.js SDK, configured with your API credentials (Account SID and Auth Token), to call Twilio's API.
- Twilio processes the request and sends the SMS message to the specified recipient number.
- Twilio returns a response with a message SID indicating success or failure.
- Express relays this status back to the original client.
Latency Expectations: Most SMS API calls complete within 200–500 ms. The actual SMS delivery time varies by carrier and geography (typically 1–30 seconds after the API call succeeds).
1. Setting Up Your Node.js Project for Twilio SMS
Initialize your Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project. Navigate into it:
bashmkdir twilio-sms-sender cd twilio-sms-sender -
Initialize Node.js Project: Initialize the project using npm. The
-yflag skips the interactive prompts.bashnpm init -y -
Install Dependencies: Install Express (web framework), the Twilio Server SDK (API client), and dotenv (environment variable loader).
bashnpm install express twilio dotenv -
Enable ES Modules: To use modern
importsyntax, open thepackage.jsonfile created in step 2 and add the"type": "module"line:json{ "name": "twilio-sms-sender", "version": "1.0.0", "description": "", "main": "index.js", "type": "module", "scripts": { "start": "node index.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "twilio": "^3.76.0", "dotenv": "^16.4.5", "express": "^4.19.2" } }Note: The dependency versions shown (
^3.76.0,^16.4.5,^4.19.2) are examples. The caret (^) symbol allows npm to install compatible minor and patch updates automatically. Runnpm install express twilio dotenvto get the latest compatible versions. -
Create
.gitignore: Create a file named.gitignorein the project root to prevent committing sensitive information and unnecessary files (likenode_modules).text# .gitignore # Dependencies node_modules/ # Environment Variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Optional Build Folders dist/ build/ # Misc .DS_Store -
Create Environment File (
.env): Create a file named.envin the project root. This file stores your Twilio API credentials and configuration. Never commit this file to version control.text# .env # Twilio API Credentials TWILIO_ACCOUNT_SID=YOUR_TWILIO_ACCOUNT_SID TWILIO_AUTH_TOKEN=YOUR_TWILIO_AUTH_TOKEN # Twilio Sender Number or ID (Use a purchased number or an Alphanumeric Sender ID) # Alphanumeric Sender IDs (e.g., "MyApp") may have restrictions. # A purchased Twilio number (e.g., 14155550100) is generally more reliable. TWILIO_SENDER_ID=YOUR_TWILIO_NUMBER_OR_SENDER_ID # Server Port PORT=3000Replace the placeholder values in Section 4.
-
Create Project Files: Create the main application file and a library file for Twilio logic.
bashtouch index.js lib.js
Your project structure should now look like this:
twilio-sms-sender/
├── .env
├── .gitignore
├── index.js
├── lib.js
├── node_modules/
├── package-lock.json
└── package.jsonTroubleshooting: If your directory structure doesn't match, verify you ran all commands from the twilio-sms-sender directory. Use ls (macOS/Linux) or dir (Windows) to check the current directory contents.
2. Implementing the Twilio SMS Sending Function
Encapsulate the Twilio interaction logic within the lib.js file.
-
Edit
lib.js: Openlib.jsand add the following code:javascript// lib.js import twilio from 'twilio'; import 'dotenv/config'; // Load environment variables from .env file // --- Configuration --- // Instantiate Twilio client using credentials from environment variables const client = twilio( process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN ); // Get the sender ID or number from environment variables const sender = process.env.TWILIO_SENDER_ID; // --- Core SMS Sending Function --- /** * Sends an SMS message using the Twilio API. * @param {string} recipient - The recipient's phone number (E.164 format recommended, e.g., +14155550101). * @param {string} message - The text message content. * @returns {Promise<object>} A promise that resolves with the Twilio API response data on success. * @throws {Error} Throws an error if the API call fails or returns an error status. */ export const sendSms = async (recipient, message) => { console.log(`Attempting to send SMS from ${sender} to ${recipient}`); if (!sender) { throw new Error('TWILIO_SENDER_ID is not set in environment variables.'); } try { // Use the client.messages.create method const messageResponse = await client.messages.create({ body: message, to: recipient, from: sender }); // Twilio returns a message object with status property // Status values: // - 'queued': Message is queued and will be sent shortly // - 'sending': Message is currently being sent to the carrier // - 'sent': Message was successfully sent to the carrier // - 'delivered': Carrier confirmed delivery to the recipient // - 'undelivered': Carrier reported delivery failure // - 'failed': Message send failed (e.g., invalid number, blocked) console.log('Message sent successfully:', messageResponse.sid); console.log('Message status:', messageResponse.status); return messageResponse; // Resolve with the full response data } catch (err) { // Catch errors during the API call itself (network issues, SDK errors, API errors) console.error('Error sending SMS via Twilio:', err); // Twilio errors include code, message, and moreInfo properties if (err.code) { throw new Error(`Twilio API Error ${err.code}: ${err.message}`); } else if (err instanceof Error) { throw err; } else { throw new Error(`Twilio API Error: ${err}`); } } };
Message Status Explained:
- queued/sending/sent: Message is in transit. You'll typically see "queued" initially.
- delivered: Carrier confirmed successful delivery (requires delivery receipts enabled).
- undelivered/failed: Message didn't reach the recipient. Check Twilio logs for details.
Pricing: Each SMS segment costs varies by destination country. US/Canada typically costs $0.0075–$0.0079 per segment. Check Twilio Pricing for current rates.
3. Creating the Express REST API Endpoint
Create the Express server and the /send endpoint in index.js.
-
Edit
index.js: Openindex.jsand add the following code:javascript// index.js import express from 'express'; import 'dotenv/config'; // Load environment variables import { sendSms } from './lib.js'; // Import our SMS sending function // --- Configuration --- const { json, urlencoded } = express; // Destructure middleware const app = express(); const port = process.env.PORT || 3000; // Use port from .env or default to 3000 // --- Middleware --- // Enable parsing of JSON request bodies app.use(json()); // Enable parsing of URL-encoded request bodies (for form submissions) app.use(urlencoded({ extended: true })); // --- API Routes --- /** * @route POST /send * @description Endpoint to send an SMS message. * @access Public (For production, add authentication) * @requestBody { "phone": "string", "message": "string" } * @responseSuccess { "success": true, "data": { ...Twilio Response... } } * @responseError { "success": false, "message": "Error description" } */ app.post('/send', async (req, res) => { // Basic Input Validation const { phone, message } = req.body; if (!phone || !message || typeof phone !== 'string' || typeof message !== 'string') { console.error('Invalid request body:', req.body); return res.status(400).json({ success: false, message: 'Invalid input. Provide "phone" (string) and "message" (string) in the request body.', }); } // Basic phone format check (very lenient, allows optional '+') // Consider a dedicated library (e.g., google-libphonenumber) for robust E.164 validation in production. if (!/^\+?[1-9]\d{1,14}$/.test(phone)) { console.warn(`Potentially invalid phone format received: ${phone}. Sending anyway, Twilio will perform final validation.`); } try { // Call the sendSms function from lib.js const result = await sendSms(phone, message); console.log('SMS Send API call successful.'); // Send success response back to the client res.status(200).json({ success: true, data: result, // Include Twilio response details }); } catch (error) { // Catch errors from sendSms (API errors, validation errors in lib.js) console.error('Failed to process /send request:', error.message); // Send error response back to the client // Determine status code based on error if possible, default to 500 const statusCode = error.message.includes('Non-Whitelisted Destination') ? 403 : error.message.includes('Invalid input') ? 400 : 500; // Basic example, refine as needed res.status(statusCode).json({ success: false, message: error.message || 'An unexpected error occurred while sending the SMS.', }); } }); // --- Basic Health Check Route --- /** * @route GET /health * @description Simple health check endpoint. * @access Public * @response { "status": "ok", "timestamp": "ISO Date String" } */ app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening for SMS requests at http://localhost:${port}`); });
Testing the API:
Using curl:
curl -X POST http://localhost:3000/send \
-H "Content-Type: application/json" \
-d '{"phone": "+14155552671", "message": "Hello from Twilio!"}'Using Postman:
- Set method to POST
- Enter URL:
http://localhost:3000/send - Go to Body tab → select "raw" → choose "JSON"
- Enter:
{"phone": "+14155552671", "message": "Hello from Twilio!"} - Click Send
4. Configuring Twilio API Credentials and Phone Numbers
Configure the application with your Twilio account details.
-
Sign Up/Log In to Twilio: Go to the Twilio Console and sign up for a free trial account or log in if you already have one.
-
Find API Key and Secret: On your Twilio Console homepage, your Account SID and Auth Token display near the top.
- Navigate to Account Settings in the left-hand menu if you can't find them immediately.
- Copy the
Account SIDandAuth Token.
Security Warning: Your Auth Token is a sensitive credential. Never commit it to version control or share it publicly. Rotate your Auth Token immediately if it's exposed. Configure rotation every 90 days as a best practice.
- Get a Twilio Number or Set Sender ID:
| Option | Pros | Cons | Best For |
|---|---|---|---|
| Purchased Number | Universal support, reliable delivery, consistent branding | Monthly cost (~$1–$2/month) | Production applications |
| Alphanumeric Sender ID | No monthly fee, custom branding | Limited country support, requires pre-registration in many regions | Specific markets with support |
| Trial Default ("Twilio") | No setup required | Poor branding, trial limitations | Initial testing only |
-
Option A (Recommended): Purchase a Number:
- Navigate to Numbers → Buy a Number in the dashboard.
- Search for and purchase a number with SMS capabilities in your desired country. Cost: typically $1–$2/month.
- Copy the purchased number (in E.164 format, e.g.,
+14155550100).
-
Option B: Use Alphanumeric Sender ID:
- You can use a custom string (e.g.,
"MyApp","Alerts") as the sender ID. However, support varies by country and carrier, and may require pre-registration. Check Twilio's Sender ID documentation for specific country regulations.
- You can use a custom string (e.g.,
-
Option C (Trial Account Default): The default sender ID for trial accounts might be
"Twilio". This works but provides minimal branding.
-
Whitelist Test Numbers (CRITICAL FOR TRIAL ACCOUNTS):
- If you use a free trial account, Twilio requires you to whitelist the phone numbers you intend to send SMS to. You cannot send messages to unverified numbers until you upgrade your account.
- Navigate to Numbers → Verified Caller IDs in the Twilio Console.
- Click Add a Verified Caller ID.
- Enter the recipient phone number you want to test with (your own mobile number is a good choice).
- Twilio sends a verification code via SMS or voice call to that number. Enter the code to confirm ownership.
- Repeat for any other numbers you need to test with during the trial period. Failure to do this results in the
"Non-Whitelisted Destination"error.
-
Update
.envFile: Open your.envfile and replace the placeholder values with your actual credentials:text# .env TWILIO_ACCOUNT_SID=YOUR_ACTUAL_ACCOUNT_SID_FROM_DASHBOARD TWILIO_AUTH_TOKEN=YOUR_ACTUAL_AUTH_TOKEN_FROM_DASHBOARD TWILIO_SENDER_ID=YOUR_PURCHASED_TWILIO_NUMBER_OR_SENDER_ID PORT=3000TWILIO_ACCOUNT_SID: Your Twilio Account SID.TWILIO_AUTH_TOKEN: Your Twilio Auth Token.TWILIO_SENDER_ID: Your purchased Twilio number (e.g.,+14155550100) or your chosen alphanumeric sender ID (e.g.,MyCompany).PORT: The port your Express server will run on (3000 is common for development).
5. Adding Error Handling for Twilio API Requests
The code already incorporates basic error handling and logging:
lib.js:- Checks the Twilio API response status (
responseData.status). - Throws specific errors using
errorTextfrom Twilio for failed sends. - Uses
console.logfor successful sends andconsole.errorfor failures or API issues. - Includes a check for the
TWILIO_SENDER_IDenvironment variable.
- Checks the Twilio API response status (
index.js:- Uses a
try...catchblock around thesendSmscall in the/sendendpoint. - Logs errors using
console.error. - Returns structured JSON error responses to the client (
{ success: false, message: ... }). - Includes basic input validation (checking for
phoneandmessage). - Sets appropriate HTTP status codes (400, 403, 500).
- Uses a
Example Error Response:
{
"success": false,
"message": "Twilio API Error 21211: The 'To' number +1234 is not a valid phone number."
}Further Improvements (Beyond Basic):
-
Structured Logging: For production, use a dedicated logging library like
pinoorwinstonto output logs in JSON format. This makes them easier to parse by log aggregation tools (e.g., Datadog, Splunk, ELK stack).bashnpm install pino pino-httpIntegrate
pino-httpas Express middleware. -
Centralized Error Handling Middleware: Create custom Express error handling middleware to standardize error responses and logging across all routes.
-
Retry Mechanisms: Implement a retry strategy with exponential backoff for specific error codes (e.g., timeouts, rate limits). Libraries like
async-retrycan help. Decide whether to retry server-side or let clients handle retries based on your use case.
Retry Decision Tree:
- Retry immediately: Never (can amplify load issues)
- Retry with backoff: Network timeouts, rate limits (429), temporary Twilio issues
- Fail immediately: Invalid credentials (20003), invalid phone number (21211), insufficient funds (21606)
6. Creating a Database Schema and Data Layer
Not Applicable for this guide.
This application is stateless. It receives a request, calls the Twilio API, and returns a response. It doesn't store information about messages sent (like status, recipient, content) in a database.
When Database Tracking Becomes Necessary:
- Audit Requirements: Compliance regulations require message history retention
- Analytics Needs: Track delivery rates, campaign performance, user engagement
- Status Tracking: Monitor delivery status via webhooks for failed message handling
- Contact Management: Store and manage recipient lists, preferences, opt-outs
If you need these features, introduce a database (e.g., PostgreSQL, MongoDB) and a data access layer (using an ORM like Prisma or Sequelize, or native drivers).
7. Securing Your Twilio SMS API
Security is crucial, even for simple APIs.
- Input Validation:
- We added basic checks in
index.jsfor the presence and type ofphoneandmessage. - Recommendation: Use a dedicated validation library like
joiorexpress-validatorfor robust schema validation, format checking (especially for phone numbers using libraries likegoogle-libphonenumber), and length limits.
Example withbashnpm install express-validatorexpress-validator:javascript// index.js (modified example) import express from 'express'; import 'dotenv/config'; import { sendSms } from './lib.js'; import { body, validationResult } from 'express-validator'; // Import validator const { json, urlencoded } = express; const app = express(); const port = process.env.PORT || 3000; app.use(json()); app.use(urlencoded({ extended: true })); // Define validation rules as an array const sendSmsValidationRules = [ body('phone') .isString().withMessage('Phone must be a string.') .notEmpty().withMessage('Phone number is required.') // Add more specific validation if needed, e.g., using a custom validator or isMobilePhone // .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format.') .trim(), // Sanitize input body('message') .isString().withMessage('Message must be a string.') .notEmpty().withMessage('Message is required.') .isLength({ min: 1, max: 1600 }).withMessage('Message cannot exceed 1600 characters.') .trim() // Sanitize input ]; // Define the route handler separately const handleSendSms = async (req, res) => { // Check for validation errors const errors = validationResult(req); if (!errors.isEmpty()) { console.error('Validation errors:', errors.array()); return res.status(400).json({ success: false, errors: errors.array() }); } // Proceed if validation passes const { phone, message } = req.body; // Basic phone format check (optional, as validator can handle it) if (!/^\+?[1-9]\d{1,14}$/.test(phone)) { console.warn(`Potentially invalid phone format received: ${phone}. Sending anyway, Twilio will perform final validation.`); } try { const result = await sendSms(phone, message); console.log('SMS Send API call successful.'); res.status(200).json({ success: true, data: result }); } catch (error) { console.error('Failed to process /send request:', error.message); const statusCode = error.message.includes('Non-Whitelisted Destination') ? 403 : error.message.includes('Invalid input') ? 400 : 500; res.status(statusCode).json({ success: false, message: error.message || 'An unexpected error occurred while sending the SMS.', }); } }; // Apply validation rules middleware before the route handler app.post('/send', sendSmsValidationRules, handleSendSms); // Health check route app.get('/health', (req, res) => { res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); }); // Start server app.listen(port, () => { console.log(`Server listening for SMS requests at http://localhost:${port}`); }); - We added basic checks in
SMS-Specific Security Considerations:
- Injection Attacks: Malicious users might inject special characters to manipulate SMS routing or billing. Always sanitize inputs and use parameterized API calls (which Twilio SDK handles).
- SMS Phishing (Smishing): Validate that message content doesn't contain suspicious links or impersonate trusted entities unless your use case requires it.
- Rate-Based Attacks: Attackers might use your API to spam users. Implement rate limiting (see below) and consider requiring authentication.
- Rate Limiting:
- Protect your API from abuse and brute-force attacks by limiting the number of requests a client can make in a given time window.
- Recommendation: Use
express-rate-limit.
Example Implementation:bashnpm install express-rate-limitjavascript// index.js (near the top, after express() instantiation) import rateLimit from 'express-rate-limit'; // ... (other imports) ... const app = express(); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: { success: false, message: 'Too many requests from this IP, please try again after 15 minutes' } }); // Apply the rate limiting middleware to all requests or specific routes app.use(limiter); // Apply to all routes // Or apply only to the send route: app.use('/send', limiter); // ... (rest of the app setup: middleware, routes, listen) ...
Distributed Rate Limiting: For multi-server deployments, use a shared store (Redis) with rate-limit-redis to synchronize rate limits across instances.
-
API Key Security:
- Never commit your
.envfile or hardcode API keys/secrets directly in your code. Use environment variables as shown. Ensure.envis listed in your.gitignore. - In production environments, use your deployment platform's mechanism for securely managing environment variables (e.g., AWS Secrets Manager, Azure Key Vault, Heroku Config Vars, Docker secrets).
- Credential Rotation: Rotate your Twilio Auth Token every 90 days. Generate a new token in Twilio Console → Account → API Keys & Tokens, update your
.env, and verify functionality before revoking the old token.
- Never commit your
-
Authentication/Authorization:
- The current
/sendendpoint is public. In a real-world scenario, protect it. - Recommendation: Implement API key authentication, JWT (JSON Web Tokens), or OAuth2 depending on your use case. The client calling
/sendneeds to provide a valid token or key in the request headers (e.g.,Authorization: Bearer <token>orX-API-Key: <key>). Add middleware to verify this before allowing access to the/sendlogic.
- The current
Example API Key Middleware:
// Simple API key authentication middleware
const authenticateApiKey = (req, res, next) => {
const apiKey = req.header('X-API-Key');
const validApiKey = process.env.API_KEY; // Store this securely
if (!apiKey || apiKey !== validApiKey) {
return res.status(401).json({
success: false,
message: 'Unauthorized: Invalid or missing API key'
});
}
next();
};
// Apply to the /send route
app.post('/send', authenticateApiKey, async (req, res) => {
// ... your existing /send handler code
});- Helmet:
- Use the
helmetmiddleware to set various HTTP headers that help protect your app from common web vulnerabilities (like XSS, clickjacking).
bashnpm install helmetjavascript// index.js (near the top) import helmet from 'helmet'; // ... (other imports) ... const app = express(); app.use(helmet()); // ... (rest of the app setup) ... - Use the
8. Phone Number Formatting and SMS Message Length
-
Phone Number Formatting: Twilio prefers the E.164 format (e.g.,
+14155550101). While it might handle other formats, standardize on E.164 using a library likegoogle-libphonenumberduring input validation to avoid ambiguity and potential delivery issues. -
Message Encoding & Length: Standard SMS messages have character limits (160 for GSM-7 encoding, 70 for UCS-2/Unicode). Longer messages split into multiple segments (concatenated SMS), which incur higher costs. The Twilio API handles concatenation automatically, but the
max: 1600validation example provides a basic safeguard.
Message Length Calculation:
// Example: Calculate SMS segments for a message
const calculateSmsSegments = (message) => {
const hasUnicode = /[^\x00-\x7F]/.test(message);
const segmentLength = hasUnicode ? 70 : 160;
const segments = Math.ceil(message.length / segmentLength);
return { segments, encoding: hasUnicode ? 'UCS-2' : 'GSM-7' };
};
const msg = "Hello! 👋 This message contains unicode.";
console.log(calculateSmsSegments(msg));
// Output: { segments: 1, encoding: 'UCS-2' }-
Sender ID Restrictions: Alphanumeric sender IDs are not universally supported and might be overwritten by carriers or require registration. Using a purchased Twilio virtual number is generally more reliable for consistent delivery and branding.
-
International Sending: Ensure your Twilio account is enabled for sending to the specific countries you target. Pricing and regulations vary significantly by country. Check Console → Messaging → Geo Permissions.
9. Optimizing Performance for High-Volume SMS Sending
For this API, performance bottlenecks are unlikely within the Node.js application itself. The main latency comes from the external network call to the Twilio API.
- Connection Pooling: The
twiliopackage handles underlying HTTP connection management efficiently. - Asynchronous Operations: Using
async/awaitensures Node.js's event loop isn't blocked during the API call to Twilio, allowing the server to handle other requests concurrently. - Caching: Not applicable here, as each request should trigger a unique SMS send.
- Load Testing: Use tools like
k6,artillery, orApacheBench(ab) to simulate traffic and identify potential bottlenecks under load. Monitor CPU, memory usage, and response times. Pay attention to Twilio API rate limits you might hit under heavy load.
Baseline Performance Metrics:
- Expected Throughput: 50–200 requests/second per instance (depends on hardware)
- Target Latency: p50 < 300 ms, p95 < 600 ms, p99 < 1000 ms
- Memory Usage: ~50–100 MB per instance at rest, ~200–300 MB under load
High-Volume Architecture: For applications sending >1000 SMS/minute, implement a queue-based architecture using Redis, RabbitMQ, or AWS SQS. This decouples API requests from Twilio API calls, providing better resilience and rate limit handling.
10. Monitoring Twilio SMS Delivery and API Performance
- Health Checks: The
/healthendpoint provides a way for load balancers or monitoring systems (like UptimeRobot, Pingdom) to check if the service is running. - Performance Metrics: Instrument your application to collect key metrics:
- Request rate (requests per second/minute).
- Request latency (how long the
/sendendpoint takes to respond). - Error rate (percentage of 5xx or 4xx errors).
- Node.js process metrics (CPU usage, memory usage, event loop lag).
- Tools: Prometheus with
prom-client, Datadog APM, New Relic.
- Logging: As mentioned in Section 5, structured logging (JSON) is essential. Ship logs to a centralized platform (Datadog Logs, AWS CloudWatch Logs, ELK Stack) for analysis and alerting.
- Error Tracking: Integrate an error tracking service (Sentry, Bugsnag, Datadog Error Tracking) to capture, aggregate, and alert on exceptions that occur in your application, providing stack traces and context.
- Twilio Dashboard: Monitor your SMS usage, delivery rates, and spending directly within the Twilio API Dashboard. Configure balance alerts.
Alert Configuration Recommendations:
| Alert Type | Condition | Threshold | Window | Action |
|---|---|---|---|---|
| High Error Rate | 5xx responses from /send | >5% | 5 minutes | Page on-call engineer |
| High Latency | p95 response time | >1 second | 10 minutes | Investigate performance |
| Low Balance | Twilio account balance | <$10 | Immediate | Alert billing team |
| Service Down | /health endpoint fails | 3 consecutive checks | 3 minutes | Page on-call engineer |
| Rate Limit Hit | Twilio 429 errors | >10 occurrences | 5 minutes | Review traffic patterns |
11. Troubleshooting Common Twilio SMS Issues
-
Error: The 'To' number is not a valid phone number(Error Code 21211):- Meaning: The recipient phone number format is invalid or not recognized by Twilio.
- Solution: Ensure the phone number is in E.164 format (e.g.,
+14155551234). Use a phone number validation library likegoogle-libphonenumberto format numbers correctly before sending.
-
Error: The number ... is unverified. Trial accounts cannot send messages to unverified numbers(Error Code 21608):- Meaning: You use a Twilio trial account and tried to send an SMS to a phone number not verified in your account.
- Solution: Go to your Twilio Console → Phone Numbers → Verified Caller IDs. Click "Add a new Caller ID" and verify the recipient number using the verification code sent to that phone. Alternatively, upgrade your account to send to any number.
-
Error: Authenticate/Error: Username and/or password are incorrect(Error Code 20003):- Meaning: The
TWILIO_ACCOUNT_SIDorTWILIO_AUTH_TOKENin your.envfile is incorrect or doesn't match your Twilio Console credentials. - Solution: Double-check the Account SID and Auth Token in your
.envfile against the values in Twilio Console → Account → Account Info. Ensure there are no typos, extra spaces, or quotes. If you regenerated your Auth Token, update it in your.envfile.
- Meaning: The
-
Error: The 'From' number ... is not a valid phone number or shortcode(Error Code 21212):- Meaning: The
TWILIO_SENDER_IDin your.envfile is not a valid Twilio phone number associated with your account, or it's formatted incorrectly. - Solution: Verify the
TWILIO_SENDER_IDis a phone number you own in your Twilio account (check Console → Phone Numbers → Manage Numbers). Ensure it's in E.164 format. Alphanumeric sender IDs are not supported in all countries – use a purchased Twilio number for reliability.
- Meaning: The
-
Error: Insufficient funds(Error Code 21606):- Meaning: Your Twilio account balance is too low to send the SMS message.
- Solution: Add credit to your Twilio account via Console → Billing. Set up auto-recharge or balance alerts to prevent service interruptions.
-
Network Errors (
ECONNREFUSED,ETIMEDOUT):- Meaning: Your server cannot establish a connection to the Twilio API endpoints.
- Solution: Check your server's internet connectivity. Ensure firewalls are not blocking outbound HTTPS connections to
api.twilio.comon port 443. Verify your network allows outbound connections to Twilio's IP ranges.
-
Environment Variables Not Loaded:
- Meaning:
process.env.TWILIO_ACCOUNT_SIDor other variables areundefined. - Solution: Ensure the
dotenvpackage is installed (npm install dotenv) andimport 'dotenv/config';is called at the very top of your entry files (lib.jsandindex.js). Verify the.envfile exists in the project root directory where you runnode index.jsand is correctly formatted (no quotes around values unless part of the value itself).
- Meaning:
-
TypeError: Cannot read property 'create' of undefined:- Meaning: The Twilio client wasn't initialized correctly, likely due to missing or invalid credentials.
- Solution: Verify your
TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENare correctly set in the.envfile and loaded before initializing the Twilio client. Check the console for authentication error messages.
-
SMS Not Received (but API shows success with
sidreturned):- Check Recipient Number: Verify the
tophone number is correct and in E.164 format (e.g.,+14155551234). - Carrier Filtering: Mobile carriers sometimes filter messages they perceive as spam, especially from new or unverified numbers. Ensure message content avoids spam trigger words. Consider registering your number and use case with Twilio for better deliverability.
- Geographic Permissions: Twilio accounts have geographic permissions that control which countries you can send to. Check Console → Messaging → Geo Permissions and enable the recipient's country if needed.
- Twilio Message Logs: Check Console → Monitor → Logs → Messaging for detailed delivery status, error codes, and carrier responses. Twilio provides extensive logging for debugging delivery issues.
- Status Callbacks: Implement Twilio webhooks to receive real-time delivery status updates (delivered, undelivered, failed). Add a
statusCallbackURL parameter to yourmessages.create()call.
- Check Recipient Number: Verify the
Webhook Implementation Example:
// Add to index.js
app.post('/status-callback', express.urlencoded({ extended: false }), (req, res) => {
const { MessageSid, MessageStatus, ErrorCode } = req.body;
console.log(`Message ${MessageSid} status: ${MessageStatus}`);
if (ErrorCode) {
console.error(`Error code: ${ErrorCode}`);
}
res.status(200).send('OK');
});
// Modify sendSms in lib.js to include statusCallback
const messageResponse = await client.messages.create({
body: message,
to: recipient,
from: sender,
statusCallback: 'https://your-domain.com/status-callback' // Replace with your webhook URL
});-
Rate Limiting (Error Code 20429):
- Meaning: You've exceeded Twilio's API rate limits for your account type.
- Solution: Implement exponential backoff retry logic. For trial accounts, rate limits are stricter. Upgrade your account for higher limits. Check Twilio's rate limit documentation for your account tier.
-
A2P 10DLC Registration (US Long Codes):
- Important for US SMS: As of 2025, sending SMS from US long code numbers (10-digit numbers) for Application-to-Person (A2P) messaging requires 10DLC registration. Unregistered numbers face reduced throughput and higher filtering.
- Registration Process:
- Register your business/brand in Twilio Console → Messaging → Regulatory Compliance → A2P 10DLC
- Submit campaign information (use case, message samples, expected volume)
- Wait for approval (typically 1-2 weeks for low-volume campaigns, up to 6 weeks for high-volume)
- Associate your 10DLC campaign with your long code phone numbers
- Alternative: For immediate sending without registration, use a Twilio toll-free number or short code instead. Note: Toll-free numbers now require verification (typically 1-5 business days) and have their own compliance requirements.
Toll-Free Verification Requirements:
- Business name and address
- Business website
- Use case description
- Message samples (opt-in, opt-out, help responses)
- Expected monthly volume
For more details, see Twilio's A2P 10DLC documentation and Toll-Free SMS documentation.
Frequently Asked Questions
How to send SMS with Node.js and Express?
Use the Vonage Node.js SDK with Express to create a REST API endpoint. This endpoint receives the recipient's phone number and message, then utilizes the Vonage API to send the SMS. The article provides a step-by-step guide for setting up this project.
What is Vonage used for in Node.js SMS?
Vonage is a communications API platform that provides the infrastructure for sending SMS messages programmatically. The Vonage Node.js SDK simplifies interaction with the Vonage API, allowing your Node.js application to send SMS messages easily.
Why does Vonage require whitelisting numbers?
For trial accounts, Vonage requires whitelisting destination numbers for security and to prevent abuse. This means you must register the recipient phone numbers in your Vonage dashboard before sending test messages to them.
When should I use an alphanumeric sender ID?
Alphanumeric sender IDs (e.g., 'MyApp') can be used for branding, but have limitations. Support varies by country and carrier, and they might be overwritten. Purchased Vonage numbers are generally more reliable.
Can I track SMS delivery status with Vonage?
Yes, you can implement Vonage webhooks to receive delivery receipts (DLRs). While not covered in the basic guide, DLRs provide detailed information on message delivery status, including whether the message reached the recipient's handset.
How to set up a Node.js Express SMS server?
Install Express, the Vonage Server SDK, and dotenv. Create an Express app with a '/send' endpoint to handle SMS requests. Configure your Vonage API credentials in a '.env' file.
What is the Vonage sender ID?
The Vonage sender ID is either a purchased virtual number or an alphanumeric string (e.g., 'MyCompany') that identifies the sender of the SMS message. Using a purchased number is recommended for reliability.
Why does my Vonage SMS send fail with 'Non-Whitelisted Destination'?
This error occurs with Vonage trial accounts when sending to unverified numbers. Add the recipient's number to your 'Test numbers' in the Vonage dashboard.
How to fix 'Authentication failed' with Vonage API?
Double-check your Vonage API key and secret in the '.env' file against your Vonage dashboard. Ensure no typos or extra spaces exist and regenerate secrets if needed.
What are best practices for Vonage SMS security?
Use environment variables for API credentials, implement input validation, and add rate limiting. Consider using helmet for HTTP header security and implement authentication/authorization for production.
How to handle long SMS messages with Vonage?
The Vonage API automatically handles long messages by splitting them into segments (concatenated SMS). Be mindful of character limits and potential costs. The example code includes a 1600-character validation as a basic safeguard.
What to do if Vonage SMS isn't received but API returns success?
Double-check the recipient number, consider carrier filtering, and implement Vonage webhooks for delivery receipts (DLRs) to track detailed delivery status.
How to handle Vonage API errors in Node.js?
Implement comprehensive error handling using try-catch blocks. Include checks for Vonage API response status and 'error-text' messages. Return informative error messages to the client with appropriate HTTP status codes.