This guide provides a comprehensive walkthrough for building a robust bulk SMS messaging application using Node.js, Express, and the Vonage Messages API. We will cover everything from project setup and core sending logic to crucial considerations like rate limiting, error handling, security, and compliance (specifically US 10DLC).
The final application will expose a simple API endpoint that accepts a list of phone numbers and a message, then efficiently sends the message to each recipient via Vonage, respecting API limits and providing basic feedback.
Project Goals:
- Create a scalable Node.js/Express backend service for sending bulk SMS messages.
- Integrate securely with the Vonage Messages API.
- Implement proper rate limiting to comply with Vonage policies.
- Include robust error handling and basic logging.
- Address critical compliance requirements like US 10DLC.
- Provide a foundation for a production-ready messaging system.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building efficient I/O-bound applications like API services.
- Express: A minimal and flexible Node.js web application framework, perfect for creating our API layer.
- Vonage Messages API: A powerful API enabling communication across multiple channels, including SMS. We use it for its flexibility and features.
@vonage/server-sdk
: The official Vonage Node.js SDK for seamless API interaction.dotenv
: To manage environment variables securely.express-rate-limit
: Middleware for rate-limiting API requests.p-limit
: A utility to limit concurrency for asynchronous operations, crucial for managing Vonage API call rates.
System Architecture:
+-----------------+ +-----------------------+ +-----------------+ +-----------------+
| Client (e.g. UI,| ---> | Node.js/Express App | ---> | Vonage Messages | ---> | Mobile Carriers | ---> End User Phones
| Script) | | (API Endpoint: | | API | | (SMS Network) |
| | | POST /bulk-sms) | +-----------------+ +-----------------+
| | | - Input Validation |
| | | - Rate Limiting (API)|
| | | - Vonage SDK Usage |
| | | - Concurrency Limit |
| | | - Error Handling |
| | | - Logging |
+-----------------+ +-----------------------+
|
| (Reads Config)
V
+-------------+
| .env File |
| (API Keys, |
| App ID, |
| Number) |
+-------------+
Prerequisites:
- A Vonage API account. Sign up here if you don't have one.
- Node.js and npm (or yarn) installed locally. We recommend the current LTS version. Download Node.js.
- A Vonage phone number capable of sending SMS messages. You can purchase one via the Vonage Dashboard or the Vonage CLI.
- Basic familiarity with Node.js, Express, and asynchronous JavaScript (
async
/await
).
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-bulk-sms cd vonage-bulk-sms
-
Initialize Node.js Project: Initialize the project using npm. Accept the defaults or customize as needed.
npm init -y
This creates a
package.json
file. -
Install Dependencies: Install Express for the web server, the Vonage SDK,
dotenv
for environment variables,express-rate-limit
for API protection, andp-limit
for controlling Vonage API call concurrency.npm install express @vonage/server-sdk dotenv express-rate-limit p-limit
-
Create Project Structure: Create the main application file and a file for environment variables.
touch index.js .env .gitignore
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing sensitive information and dependencies.# .gitignore node_modules .env
-
Set up Environment Variables (
.env
): Open the.env
file and add placeholders for your Vonage credentials and configuration. We will populate these in the next section.# .env # Vonage Application Credentials VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Or the full path to your key # Vonage API Key/Secret (Optional) # While the SDK uses Application ID/Private Key for Messages API, # Key/Secret can be useful for the Vonage CLI or other API interactions. # VONAGE_API_KEY=YOUR_VONAGE_API_KEY # VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET # Vonage Number to send messages FROM (in E.164 format, e.g., 12015550123) VONAGE_NUMBER=YOUR_VONAGE_NUMBER # Port for the Express server PORT=3000 # Vonage API Concurrency Limit # Adjust based on Vonage account limits (overall & per-number), see docs & Caveats section. # Also consider per-number throughput limits (e.g., 10DLC). VONAGE_CONCURRENCY_LIMIT=25
Why .env
? Storing credentials and configuration separate from code is crucial for security and flexibility across different environments (development, staging, production).
2. Vonage Configuration and Credentials
Before writing code, we need to configure our Vonage account, create a Vonage Application, and obtain the necessary credentials.
-
Log in to Vonage Dashboard: Access your Vonage API Dashboard.
-
Verify SMS API Settings:
- Navigate to API settings in the left-hand menu.
- Scroll down to SMS settings.
- Ensure 'Default SMS Setting' is set to Messages API. While the
@vonage/server-sdk
specifically uses the Messages API for sending, this setting ensures consistent behavior, especially for features like handling incoming SMS if you later configure webhooks outside of a specific Vonage Application context. It also aligns dashboard defaults with the API used by the SDK. Click Save changes if you modify this.
-
Create a Vonage Application: The Messages API uses Application authentication (Application ID + Private Key).
- Navigate to Applications > Create a new application.
- Enter an Application name (e.g.,
""Bulk SMS Service""
). - Click Generate public and private key. Immediately save the
private.key
file that downloads. Place this file in your project's root directory (or specify the correct path inVONAGE_PRIVATE_KEY_PATH
in your.env
file). Vonage does not store this key, so keep it safe. - Enable the Messages capability.
- Enter placeholder URLs for Inbound URL and Status URL (e.g.,
https://example.com/webhooks/inbound
,https://example.com/webhooks/status
). Even if you aren't receiving messages in this specific app, the Status URL is useful for delivery receipts if you implement that later. These URLs must be HTTPS. - Click Generate new application.
-
Link Your Vonage Number:
- On the application configuration page, find the Link virtual numbers section.
- Click Link next to the Vonage number you intend to send messages from. This connects incoming messages (if any) and potentially other features to this application.
-
Retrieve Credentials:
- Application ID: Find this on the application's configuration page you just created.
- Private Key Path: This is the path to the
private.key
file you saved (e.g.,./private.key
if it's in the root). - Vonage Number: The virtual number you linked, in E.164 format (e.g.,
14155550100
). - (Optional) API Key and Secret: Find these at the top of the main Vonage API Dashboard page.
-
Update
.env
File: Replace the placeholder values in your.env
file with the actual credentials you just obtained. EnsureVONAGE_PRIVATE_KEY_PATH
points correctly to your savedprivate.key
file.
3. Implementing Core Functionality: Sending Bulk SMS
Now, let's write the core logic to interact with the Vonage API and send messages.
-
Initialize Dependencies in
index.js
: Openindex.js
and require the necessary modules. Load environment variables usingdotenv
. Initialize the Vonage SDK using your Application ID and Private Key path.// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const pLimit = require('p-limit'); // Import p-limit // --- Vonage Initialization --- const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH, }); // --- Concurrency Limiter for Vonage API calls --- const DEFAULT_CONCURRENCY = 25; // Default concurrency limit let concurrencyLimit = parseInt(process.env.VONAGE_CONCURRENCY_LIMIT, 10); if (isNaN(concurrencyLimit) || concurrencyLimit <= 0) { console.warn(`Invalid or missing VONAGE_CONCURRENCY_LIMIT in .env_ defaulting to ${DEFAULT_CONCURRENCY}.`); concurrencyLimit = DEFAULT_CONCURRENCY; } const limit = pLimit(concurrencyLimit); console.log(`Vonage API concurrency limit set to: ${concurrencyLimit}`); // Note: This limits *this application's* concurrent requests to Vonage_ // helping stay under the *overall* Vonage API rate limit (e.g._ 30/sec). // However_ it does *not* directly manage per-number throughput limits (e.g._ 1 SMS/sec for standard US 10DLC). // Exceeding per-number limits can still result in carrier filtering_ even if under the overall limit. // You may need to set VONAGE_CONCURRENCY_LIMIT lower than the overall API limit // if using only a few sending numbers_ to respect their individual throughput. const FROM_NUMBER = process.env.VONAGE_NUMBER; // --- Express App Setup (will add routes later) --- const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); // Middleware to parse JSON bodies app.use(express.urlencoded({ extended: true })); // Middleware for URL-encoded bodies // --- Health Check Endpoint --- app.get('/health'_ (req_ res) => { res.status(200).send('OK'); }); // --- Main Sending Logic (Function) --- async function sendBulkSms(recipients, message) { if (!Array.isArray(recipients) || recipients.length === 0) { throw new Error('Recipients must be a non-empty array.'); } if (typeof message !== 'string' || message.trim() === '') { throw new Error('Message must be a non-empty string.'); } if (!FROM_NUMBER) { throw new Error('VONAGE_NUMBER environment variable is not set.'); } console.log(`Attempting to send message to ${recipients.length} recipients...`); const results = []; // Create an array of promises, each wrapped by the limiter const sendPromises = recipients.map(recipient => limit(async () => { // Basic phone number cleanup - REMOVES non-digits. // WARNING: This is insufficient for proper E.164 validation/formatting, especially internationally. // Use a library like libphonenumber-js in production (see Section 8). const cleanedRecipient = recipient.replace(/\D/g, ''); try { const resp = await vonage.messages.send({ message_type: 'text', to: cleanedRecipient, // Use cleaned number from: FROM_NUMBER, channel: 'sms', text: message, }); console.log(`Message sent to ${cleanedRecipient}, UUID: ${resp.message_uuid}`); results.push({ recipient: cleanedRecipient, status: 'success', message_uuid: resp.message_uuid }); return resp; // Return the response for potential further processing } catch (err) { // Handle potential Vonage API errors let errorMessage = `Failed to send to ${cleanedRecipient}: Unknown error`; if (err.response && err.response.data) { // Log more detailed error from Vonage if available errorMessage = `Failed to send to ${cleanedRecipient}: ${err.response.data.title || err.message} (Status: ${err.response.status})`; console.error('Vonage API Error:', JSON.stringify(err.response.data, null, 2)); } else { console.error(`Error sending to ${cleanedRecipient}:`, err.message); } results.push({ recipient: cleanedRecipient, status: 'error', error: errorMessage }); // We capture the error but don't re-throw, allowing other messages to proceed return null; // Indicate failure for this specific message } }) ); // Wait for all limited promises to resolve await Promise.all(sendPromises); console.log('Bulk send process completed.'); const successCount = results.filter(r => r.status === 'success').length; const errorCount = results.length - successCount; console.log(`Summary: ${successCount} successful, ${errorCount} failed.`); return results; // Return the detailed results array } // --- Start the Server (will add API route before this) --- // app.listen(PORT, () => { // console.log(`Server running on http://localhost:${PORT}`); // });
- Explanation:
- We load environment variables first.
- The Vonage SDK is initialized with the Application ID and private key path from
.env
. p-limit
is initialized with a concurrency value from.env
(defaulting to 25). This controls the application's outbound request rate to Vonage.- The
sendBulkSms
function takes an array ofrecipients
and amessage
. - It validates inputs.
- It iterates through recipients using
map
. Each call tovonage.messages.send
is wrapped inlimit()
, ensuring no more thanconcurrencyLimit
calls run concurrently. - Basic phone number cleaning is included, but flagged as insufficient for production (see Section 8).
- The
vonage.messages.send
method sends the SMS. We specifymessage_type: 'text'
,to
,from
,channel: 'sms'
, and thetext
. - Successful sends are logged with the
message_uuid
(useful for tracking). - Errors during sending are caught per message. Detailed error info from Vonage (if available in
err.response.data
) is logged. The error is recorded, but the loop continues for other recipients. Promise.all
waits for all throttled send operations to complete.- A summary and a detailed results array are returned.
4. Building the API Layer
Now, let's create the Express API endpoint to trigger the bulk sending process.
-
Add API Rate Limiting: Before defining the route, configure
express-rate-limit
to protect your API endpoint from abuse. Add this near the top ofindex.js
, afterapp.use(express.urlencoded...)
.// index.js (continued) const rateLimit = require('express-rate-limit'); // --- API Rate Limiter --- const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the rate limiting middleware to API calls only // If you have other non-API routes, apply selectively app.use('/bulk-sms', apiLimiter); // Apply limiter specifically to the bulk endpoint
- Why Rate Limit? This prevents a single user or script from overwhelming your service (and potentially Vonage), incurring costs, or causing denial-of-service. Adjust
windowMs
andmax
based on expected usage.
-
Create the
/bulk-sms
Endpoint: Add the POST route handler inindex.js
below the rate limiter setup and beforeapp.listen
.// index.js (continued) // --- API Endpoint for Bulk SMS --- app.post('/bulk-sms', async (req, res) => { const { recipients, message } = req.body; // --- Basic Input Validation --- if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ error: 'Invalid input: ""recipients"" must be a non-empty array.' }); } if (!message || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ error: 'Invalid input: ""message"" must be a non-empty string.' }); } // Add more validation as needed (e.g., max recipients per request, phone number format) try { console.log(`Received bulk SMS request for ${recipients.length} recipients.`); const results = await sendBulkSms(recipients, message); // Determine overall status based on results const successCount = results.filter(r => r.status === 'success').length; const errorCount = results.length - successCount; const responsePayload = { summary: { total_recipients: recipients.length, successful: successCount, failed: errorCount, }, details: results, // Include detailed status per recipient }; // Send appropriate status code based on outcome if (errorCount > 0 && successCount === 0) { // All failed res.status(500).json(responsePayload); } else if (errorCount > 0) { // Partial success res.status(207).json(responsePayload); // 207 Multi-Status } else { // All succeeded res.status(200).json(responsePayload); } } catch (error) { // Catch errors from sendBulkSms setup or validation console.error('Error processing /bulk-sms request:', error.message); res.status(500).json({ error: 'Internal Server Error', details: error.message }); } }); // --- Start the Server --- app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT} - Ready to accept requests.`); console.log(`Bulk SMS Endpoint: POST http://localhost:${PORT}/bulk-sms`); console.log(`Health Check: GET http://localhost:${PORT}/health`); });
- Explanation:
- The route listens for POST requests at
/bulk-sms
. - It expects a JSON body with
recipients
(array of strings) andmessage
(string). - Basic validation checks if the inputs exist and are of the correct type. Production apps should use libraries like
express-validator
for more robust validation. - It calls our
sendBulkSms
function. - Based on the
results
array, it calculates success/failure counts. - It returns a JSON response summarizing the outcome and providing detailed status for each recipient.
- Appropriate HTTP status codes are used: 200 (All Success), 207 (Partial Success), 500 (All Failed or Internal Error), 400 (Bad Request/Input Validation Failed).
- The server start logic is now included.
- The route listens for POST requests at
-
Testing the Endpoint: You can now test the endpoint using
curl
or a tool like Postman.-
Start the server:
node index.js
-
Send a request using
curl
: (ReplaceYOUR_TEST_NUMBER_1/2
with actual phone numbers in E.164 format)curl -X POST http://localhost:3000/bulk-sms \ -H ""Content-Type: application/json"" \ -d '{ ""recipients"": [""YOUR_TEST_NUMBER_1"", ""YOUR_TEST_NUMBER_2""], ""message"": ""Hello from the Bulk SMS App! (Test)"" }'
-
Expected Response (Example - Partial Success):
{ ""summary"": { ""total_recipients"": 2, ""successful"": 1, ""failed"": 1 }, ""details"": [ { ""recipient"": ""YOUR_TEST_NUMBER_1"", ""status"": ""success"", ""message_uuid"": ""abcdabcd-1234-5678-90ab-xyzxyzxyzxyz"" }, { ""recipient"": ""YOUR_TEST_NUMBER_2"", ""status"": ""error"", ""error"": ""Failed to send to YOUR_TEST_NUMBER_2: The target number is invalid or does not exist (Status: 400)"" } ] }
Note:
YOUR_TEST_NUMBER_1
andYOUR_TEST_NUMBER_2
in the response above are placeholders representing the numbers you sent to.You should also see detailed logs in the terminal where
node index.js
is running.
-
5. Error Handling, Logging, and Retries (Enhancements)
Our current implementation has basic error handling. Let's discuss improvements.
- Consistent Error Strategy: We already catch errors per-message and log them. The API returns different status codes based on overall success. This is a decent start. For production, consider standardizing error response formats.
- Logging:
console.log
is used here. For production, use a dedicated logging library like Winston or Pino. These enable:- Different log levels (debug, info, warn, error).
- Structured logging (JSON format) for easier parsing by log management tools (e.g., Datadog, Splunk, ELK stack).
- Outputting logs to files or external services.
- Retry Mechanisms:
- Vonage Retries: Vonage often retries sending messages internally if temporary network issues occur.
- Application-Level Retries: For transient errors (like network timeouts to Vonage or specific 4xx/5xx errors that might be temporary), you could implement retries within the
catch
block of thelimit(async () => { ... })
call.- Use libraries like
async-retry
orp-retry
. - Implement exponential backoff: Wait longer between retries (e.g., 1s, 2s, 4s) to avoid hammering the API during outages.
- Caution: Don't retry errors that are clearly permanent (e.g., invalid number format, insufficient funds, authentication failure). Retrying these wastes resources. Focus retries on potential network issues or Vonage server errors (5xx).
- Use libraries like
Example Sketch (using pseudo-retry logic):
You could use libraries like async-retry
or p-retry
for this. The following pseudo-code illustrates the concept, assuming helper functions like shouldRetry
, sleep
, calculateBackoff
, and recordFailure
are defined:
// Inside the catch block of the send attempt
catch (err) {
if (shouldRetry(err)) { // Function to check if error is retryable (e.g., 5xx status)
let retries = 3;
let lastError = err;
while (retries > 0) {
await sleep(calculateBackoff(retries)); // sleep(ms), calculateBackoff(attempt)
try {
// Attempt sending again...
// If success, break loop and record success
const resp = await vonage.messages.send(/* ... */);
recordSuccess(recipient, resp.message_uuid); // Need a success recording function
lastError = null; // Clear error if succeeded
break;
} catch (retryErr) {
lastError = retryErr; // Store the latest error
retries--;
if (retries === 0) {
// Log final failure after retries
console.error(`Final retry attempt failed for ${recipient}`);
recordFailure(recipient, lastError);
}
}
}
} else {
// Log non-retryable error immediately
recordFailure(recipient, err);
}
}
6. Database Schema and Data Layer (Considerations)
While this guide doesn't implement a database, a production system needs one for tracking and auditing.
-
Why? To store message status, recipient details, timestamps, Vonage
message_uuid
, delivery receipts (if using webhooks), costs, and potential errors persistently. -
Schema Example (Conceptual - e.g., PostgreSQL):
CREATE TABLE messages ( id SERIAL PRIMARY KEY, recipient VARCHAR(20) NOT NULL, sender VARCHAR(20) NOT NULL, -- Your Vonage number message_body TEXT NOT NULL, vonage_message_uuid UUID UNIQUE, -- Store the UUID returned by Vonage status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, sent, delivered, failed, rejected error_message TEXT, submitted_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, last_updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -- Add campaign_id, user_id etc. if needed ); CREATE INDEX idx_messages_recipient ON messages(recipient); CREATE INDEX idx_messages_status ON messages(status); CREATE INDEX idx_messages_submitted_at ON messages(submitted_at);
-
Implementation:
- Use an ORM like Prisma or Sequelize to interact with the database from Node.js.
- In
sendBulkSms
, before sending, insert a record withstatus: 'submitted'
. - After a successful API call, update the record with the
vonage_message_uuid
andstatus: 'sent'
. - On API error, update the record with
status: 'failed'
and theerror_message
. - (Advanced) Set up a separate webhook endpoint (
/webhooks/status
) to receive Delivery Receipts (DLRs) from Vonage and update the message status accordingly (delivered
,failed
,rejected
). This requires configuring the Status URL in your Vonage Application.
7. Security Features
Security is paramount.
- Input Validation: We have basic checks. Use
express-validator
for:- Ensuring
recipients
is an array of strings matching E.164 format (using libraries likelibphonenumber-js
). - Sanitizing the
message
content (e.g., stripping potentially harmful characters, though less critical for pure SMS text unless input comes from untrusted sources). - Checking message length limits.
- Limiting the maximum number of recipients per API call.
- Ensuring
- Authentication & Authorization: The
/bulk-sms
endpoint is currently open. Secure it!- API Keys: Generate unique keys for clients. Require clients to send the key in a header (e.g.,
X-API-Key
). Validate the key on the server. - JWT: If users are involved, use JSON Web Tokens for authentication.
- API Keys: Generate unique keys for clients. Require clients to send the key in a header (e.g.,
- Rate Limiting: Already implemented at the IP level. Consider adding user-specific or API-key-specific rate limits for finer control.
- HTTPS: Always run your Node.js application behind a reverse proxy (like Nginx or Caddy) configured for HTTPS in production. Never expose Node.js directly to the internet without TLS.
- Dependency Management: Regularly update dependencies (
npm audit
,npm update
) to patch known vulnerabilities. Use tools like Snyk. - Secrets Management: Never commit
.env
files or private keys to Git. Use environment variables provided by your deployment platform or a secrets management service (like HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager). Ensure theprivate.key
file has restrictive read permissions.
8. Handling Special Cases
-
Phone Number Formatting: Always normalize numbers to E.164 format (
+
followed by country code and number, no spaces or symbols) before sending to Vonage. Libraries likelibphonenumber-js
are highly recommended for parsing, validating, and formatting numbers robustly. Our basic cleanup (.replace(/\D/g, '')
) is insufficient for production.- Example using
libphonenumber-js
(requiresnpm install libphonenumber-js
):
// Conceptual example within sendBulkSms loop // const { parsePhoneNumberFromString } = require('libphonenumber-js'); // const phoneNumber = parsePhoneNumberFromString(recipient /*, 'US' Optional default country */); // if (phoneNumber && phoneNumber.isValid()) { // const e164Number = phoneNumber.format('E.164'); // e.g., +12125552368 // // Use e164Number when calling vonage.messages.send // } else { // console.warn(`Invalid phone number format skipped: ${recipient}`); // // Record error or skip // continue; // Or handle differently // }
- Example using
-
Character Encoding: The Vonage Messages API generally handles Unicode (like emojis) correctly when using the SDK. Be mindful if constructing raw requests.
-
Message Length & Concatenation: Standard SMS messages are 160 GSM-7 characters or 70 UCS-2 characters (for Unicode). Longer messages are split (concatenated SMS) and billed as multiple parts. Vonage handles this, but be aware of billing implications.
-
Regulations (CRITICAL - US 10DLC):
- If sending Application-to-Person (A2P) SMS to US numbers using standard 10-digit long codes (10DLC), you must register your Brand and Campaign(s) through Vonage (or your provider).
- Failure to register leads to message blocking and potential fines by carriers.
- Registration involves describing your use case (e.g., 2FA, marketing alerts, customer care).
- Throughput limits (messages per second/minute/day) are assigned based on your Brand/Campaign registration score. This directly impacts how many messages you can send from your 10DLC numbers.
- This is not optional for US A2P traffic. Start the 10DLC registration process early.
-
Other Countries: Different countries have varying regulations regarding sender IDs (alphanumeric vs. numeric), content restrictions, and opt-in requirements. Research the rules for your target countries.
9. Performance Optimizations
- Concurrency Control: We've implemented this with
p-limit
. Monitor Vonage API responses for 429 (Too Many Requests) errors and adjustVONAGE_CONCURRENCY_LIMIT
. Remember the dual limits: Vonage's overall API limit (e.g., 30 requests/second) and per-number throughput limits (e.g., 1 SMS/sec for standard US 10DLC, higher for Toll-Free/Short Code). Yourp-limit
setting manages requests towards the overall limit, but you must also ensure you don't exceed the per-number limit. This might require using multiple Vonage numbers and distributing traffic, or settingVONAGE_CONCURRENCY_LIMIT
lower than the overall limit if using few numbers. - Asynchronous Operations: Node.js is inherently non-blocking. Ensure all I/O operations (Vonage calls, potential database interactions) use
async
/await
or Promises correctly to avoid blocking the event loop. - Resource Usage: Monitor CPU and memory usage of your Node.js process under load. Use tools like
pm2
for process management and monitoring in production. - Database Optimization: If using a database (Section 6), ensure proper indexing on frequently queried columns (
recipient
,status
,vonage_message_uuid
, timestamps). Use connection pooling.
10. Monitoring, Observability, and Analytics
- Logging: As mentioned (Section 5), use structured logging and send logs to a central management system.
- Metrics: Track key performance indicators (KPIs) like:
- Number of messages sent (total, per recipient, per campaign).
- Success/failure rates.
- API latency (both your endpoint and Vonage API response times).
- Error types and frequencies.
- Concurrency limiter usage.
- System resource usage (CPU, memory).
- Use tools like Prometheus/Grafana or Datadog APM.