Build a Node.js Express bulk SMS broadcast system with Vonage
This guide provides a complete walkthrough for building a system to send bulk SMS broadcasts using Node.js, Express, and the Vonage Messages API. We'll cover everything from initial project setup to deployment and monitoring, focusing on best practices for handling large volumes of messages, ensuring security, and managing potential issues like rate limits and regulatory compliance. While the initial implementation provides core functionality, we will emphasize the necessary steps (like queuing and compliance) to truly scale this for production loads.
By the end of this tutorial, you will have a functional Node.js application capable of accepting a list of phone numbers and a message, then broadcasting that message via SMS using Vonage. You will also understand the critical considerations for scaling this system, handling errors robustly, and complying with regulations like 10DLC for US traffic.
Project overview and goals
What we're building: A Node.js application with an Express API endpoint that receives a list of recipient phone numbers and a message body. It then uses the Vonage Messages API to send the specified message as an SMS to each recipient.
Problem solved: This system addresses the need to efficiently send the same SMS message to a large group of recipients, a common requirement for notifications, marketing campaigns, or alerts.
Technologies used:
- Node.js: A JavaScript runtime ideal for building scalable network applications.
- Express: A minimal and flexible Node.js web application framework for creating the API layer.
- Vonage Messages API: A unified API for sending messages across various channels, including SMS. We'll use the
@vonage/server-sdk
for Node.js. - dotenv: To manage environment variables securely.
System architecture:
+-------------+ +---------------------+ +----------------+ +-----------+
| Client |----->| Node.js/Express API |----->| Vonage API Gateway |----->| Recipient |
| (e.g., curl,| | (Broadcast App) | | (Messages API) | | Phone |
| Postman) | +---------------------+ +----------------+ +-----------+
+-------------+ | ^
| | (Rate Limits, Errors)
v |
+-------------+
| Logging |
+-------------+
Prerequisites:
- A Vonage API account. Sign up here.
- Your Vonage Application ID and Private Key file (generated when creating a Vonage Application).
- Node.js and npm (or yarn) installed. Download Node.js here.
- A Vonage phone number capable of sending SMS. Buy a number here or via the CLI.
- (Optional but recommended) Vonage CLI:
npm install -g @vonage/cli
- (Optional but recommended for webhook testing) ngrok: Exposes local servers to the internet.
Expected outcome: A functional API endpoint (POST /broadcast
) that triggers bulk SMS sending via Vonage, with foundational error handling and logging. You'll also have a clear understanding of scaling limitations and necessary production considerations (10DLC, rate limiting, queuing).
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: This creates a
package.json
file to manage project metadata and dependencies.npm init -y
(The
-y
flag accepts default settings) -
Install Dependencies: We need Express for the web server, the Vonage Server SDK to interact with the API, and
dotenv
to manage environment variables.npm install express @vonage/server-sdk dotenv
-
Set Up Environment Variables: Create a
.env
file in the root of your project to store sensitive credentials. Never commit this file to version control.touch .env
Create a
.env.example
file to show required variables (this can be committed).touch .env.example
Populate
.env.example
with the following:# .env.example # Vonage Credentials - Obtain from Vonage Dashboard -> API Settings & Applications VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Relative path to your private key file VONAGE_FROM_NUMBER=YOUR_VONAGE_SMS_ENABLED_NUMBER # Your purchased Vonage number
Now, copy
.env.example
to.env
and fill in your actual credentials. Ensure yourprivate.key
file (downloaded during Vonage Application creation) is placed in the location specified byVONAGE_PRIVATE_KEY_PATH
. -
Configure
.gitignore
: Create a.gitignore
file to prevent sensitive files and unnecessary directories from being tracked by Git.touch .gitignore
Add the following lines:
# .gitignore node_modules .env private.key # Ensure your private key is not committed npm-debug.log* yarn-debug.log* yarn-error.log*
-
Project Structure: Your project should now look something like this:
vonage-bulk-sms/ ├── .env ├── .env.example ├── .gitignore ├── node_modules/ ├── package.json ├── package-lock.json └── private.key # Or wherever you placed it
We will add
.js
files for our application logic next.
Why this setup?
npm init
standardizes the project structure.dotenv
keeps sensitive API keys and configurations out of the codebase, enhancing security..gitignore
prevents accidental exposure of secrets and keeps the repository clean.
2. Implementing core functionality
The core logic involves initializing the Vonage SDK and creating a function to iterate through recipients and send messages.
-
Create
vonageClient.js
: This module initializes the Vonage client using credentials from environment variables.touch vonageClient.js
Add the following code:
// vonageClient.js require('dotenv').config(); // Load environment variables from .env file const { Vonage } = require('@vonage/server-sdk'); const fs = require('fs'); // Validate essential environment variables if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) { console.error('Error: VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH must be set in .env'); process.exit(1); // Exit if essential config is missing } // Read the private key let privateKey; try { privateKey = fs.readFileSync(process.env.VONAGE_PRIVATE_KEY_PATH); } catch (err) { console.error(`Error reading private key from ${process.env.VONAGE_PRIVATE_KEY_PATH}:`, err); process.exit(1); } // Initialize Vonage client const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, // Optional, but good practice apiSecret: process.env.VONAGE_API_SECRET, // Optional, but good practice applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: privateKey }); console.log('Vonage client initialized successfully.'); module.exports = vonage;
- Why
dotenv.config()
first? Ensures environment variables are loaded before use. - Why validation? Catches configuration errors early.
- Why read
privateKey
? The SDK expects the key content, not just the path. - Why
module.exports
? Makes the initialized client reusable in other parts of the application.
- Why
-
Create
broadcastService.js
: This module contains the logic for sending messages in bulk.touch broadcastService.js
Add the following code:
// broadcastService.js const vonage = require('./vonageClient'); /** * Sends an SMS message to a list of recipients using Vonage Messages API. * IMPORTANT: This simple iteration does NOT handle rate limits effectively for large volumes. * For production at scale, implement proper queuing and rate limiting (see Section 9). * * @param {string[]} recipients - Array of phone numbers in E.164 format (e.g., '14155552671'). * @param {string} message - The text message content. * @param {string} fromNumber - The Vonage sender number from .env. * @returns {Promise<object[]>} - A promise that resolves with an array of results for each attempt. */ async function sendBulkSms(recipients, message, fromNumber) { if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { throw new Error('Invalid recipients array provided.'); } if (!message || typeof message !== 'string' || message.trim() === '') { throw new Error('Invalid message content provided.'); } if (!fromNumber) { throw new Error('Vonage sender number (fromNumber) is required.'); } const results = []; console.log(`Starting bulk SMS send to ${recipients.length} recipients...`); // Simple sequential sending loop - prone to rate limiting on large lists! for (const recipient of recipients) { const recipientNumber = recipient.trim(); // Ensure no leading/trailing whitespace // Basic validation for E.164 format (adjust regex as needed for stricter validation) if (!/^\+?[1-9]\d{1,14}$/.test(recipientNumber)) { console.warn(`Skipping invalid phone number format: ${recipientNumber}`); results.push({ to: recipientNumber, status: 'skipped', error: 'Invalid phone number format' }); continue; // Skip to the next recipient } try { console.log(`Sending SMS to ${recipientNumber}...`); const resp = await vonage.messages.send({ message_type: ""text"", text: message, to: recipientNumber, // Use validated number from: fromNumber, channel: ""sms"" }); console.log(` Message submitted to ${recipientNumber}, UUID: ${resp.message_uuid}`); results.push({ to: recipientNumber, status: 'submitted', message_uuid: resp.message_uuid }); // **WARNING:** Lack of delay here will quickly hit rate limits. // Add a small delay for demonstration, but use proper queueing in production. await new Promise(resolve => setTimeout(resolve, 200)); // e.g., 200ms delay } catch (err) { console.error(` Failed to send SMS to ${recipientNumber}:`, err?.response?.data || err.message || err); // Extract more specific error details if available from Vonage response const errorDetail = err?.response?.data || { message: err.message }; results.push({ to: recipientNumber, status: 'failed', error: errorDetail }); } } console.log('Bulk SMS sending process completed.'); return results; } module.exports = { sendBulkSms };
- Why
async/await
? Simplifies handling promises from the Vonage SDK. - Why basic validation? Prevents sending to clearly invalid numbers and wasting API calls. E.164 format (
+14155552671
) is recommended. - Why
try...catch
inside the loop? Allows the process to continue even if one message fails, logging the specific error. - Why the
results
array? Provides feedback on the status of each individual send attempt. - CRITICAL CAVEAT: The simple
for...of
loop with a smallsetTimeout
is not suitable for high-volume production use. It will quickly hit Vonage API rate limits (around 30 requests/sec by default, potentially lower depending on number type and destination) and carrier limits (10DLC). Real applications need robust queuing and rate-limiting strategies (see Section 9 and 11).
- Why
3. Building a complete API layer
We'll use Express to create a simple API endpoint to trigger the broadcast.
-
Create
server.js
: This file sets up the Express server and defines the API route.touch server.js
Add the following code:
// server.js require('dotenv').config(); const express = require('express'); const { sendBulkSms } = require('./broadcastService'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware to parse JSON bodies app.use(express.json()); // Basic request logging middleware app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.originalUrl}`); next(); }); // API Endpoint: POST /broadcast app.post('/broadcast', async (req, res) => { const { recipients, message } = req.body; const fromNumber = process.env.VONAGE_FROM_NUMBER; // Basic Input Validation if (!recipients || !Array.isArray(recipients) || recipients.length === 0) { return res.status(400).json({ error: 'Missing or invalid `recipients` array in request body.' }); } if (!message || typeof message !== 'string' || message.trim() === '') { return res.status(400).json({ error: 'Missing or invalid `message` string in request body.' }); } if (!fromNumber) { console.error('FATAL: VONAGE_FROM_NUMBER is not set in environment variables.'); return res.status(500).json({ error: 'Server configuration error: Sender number not set.' }); } try { console.log(`Received broadcast request for ${recipients.length} recipients.`); // Trigger the bulk send - Note: This is async but we await it here for simplicity. // In a real app, you might return 202 Accepted and process in the background (queue). const results = await sendBulkSms(recipients, message, fromNumber); console.log('Broadcast processing finished. Sending response.'); // Respond with the results of each send attempt res.status(200).json({ message: 'Broadcast processing initiated. See results for details.', results: results }); } catch (error) { console.error('Error during broadcast request processing:', error); res.status(500).json({ error: 'Internal Server Error', details: error.message }); } }); // Simple health check endpoint app.get('/health', (req, res) => { res.status(200).send('OK'); }); // Start the server app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); if (!process.env.VONAGE_FROM_NUMBER) { console.warn('Warning: VONAGE_FROM_NUMBER environment variable is not set.'); } });
- Why
express.json()
? Parses incoming JSON request bodies (req.body
). - Why input validation? Prevents errors caused by malformed requests.
- Why
async
route handler? Needed to useawait
when callingsendBulkSms
. - Why return
results
? Gives the client immediate feedback on which sends were attempted and their initial status (submitted, failed, skipped). - Why
/health
endpoint? Useful for monitoring and load balancers. - Production Note: Returning a 200 OK immediately after starting the process (using a queue) is often better for user experience in high-volume scenarios, rather than waiting for all sends to complete.
- Why
-
Testing the API:
- Start the server:
node server.js
- Use
curl
or Postman to send a POST request:
Curl Example:
curl -X POST http://localhost:3000/broadcast \ -H ""Content-Type: application/json"" \ -d '{ \ ""recipients"": [""+14155550100"", ""+14155550101"", ""invalid-number"", ""+14155550102""], \ ""message"": ""Hello from the Vonage Bulk SMS Test!"" \ }'
(Replace phone numbers with valid test numbers in E.164 format)
Expected JSON Response (Example):
{ ""message"": ""Broadcast processing initiated. See results for details."", ""results"": [ { ""to"": ""+14155550100"", ""status"": ""submitted"", ""message_uuid"": ""aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"" }, { ""to"": ""+14155550101"", ""status"": ""submitted"", ""message_uuid"": ""ffffffff-gggg-hhhh-iiii-jjjjjjjjjjjj"" }, { ""to"": ""invalid-number"", ""status"": ""skipped"", ""error"": ""Invalid phone number format"" }, { ""to"": ""+14155550102"", ""status"": ""failed"", ""error"": { ""type"": ""https://developer.nexmo.com/api-errors/messages-olympus#throttled"", ""title"": ""Throttled"", ""detail"": ""You have exceeded the submission capacity allowed on this account. Please wait and retry"", ""instance"": ""bf0ca934-10b5-48a0-9520-c83566617b5a"" } } ] }
(Note: The
failed
example shows a typical rate limit error) - Start the server:
4. Integrating with Vonage (Credentials)
Properly obtaining and configuring Vonage credentials is vital.
-
API Key and Secret:
- Navigate to the Vonage API Dashboard.
- Your API Key and API Secret are displayed at the top.
- Copy these values into the
VONAGE_API_KEY
andVONAGE_API_SECRET
fields in your.env
file. Although the Messages API primarily uses App ID/Private Key for sending, having these set is good practice and might be needed for other Vonage SDK functions or account management via CLI.
-
Application ID and Private Key: The Messages API requires a Vonage Application for authentication.
- Go to
Your Applications
in the Vonage Dashboard. - Click
Create a new application
. - Give it a name (e.g.,
Node Bulk SMS Broadcaster
). - Click
Generate public and private key
. Immediately save theprivate.key
file that downloads. Store it securely within your project (e.g., in the root, as configured in.env
). - Enable the
Messages
capability. - You'll need to provide webhook URLs for
Inbound URL
andStatus URL
even if you don't implement handlers immediately. You can use placeholders likehttps://example.com/webhooks/inbound
andhttps://example.com/webhooks/status
for now. If you plan to track delivery receipts, use your actual ngrok or deployed server URLs here (see Section 10). - Click
Generate new application
. - Copy the generated
Application ID
. - Paste the Application ID into
VONAGE_APPLICATION_ID
in your.env
file. - Ensure
VONAGE_PRIVATE_KEY_PATH
in.env
correctly points to the location where you savedprivate.key
.
- Go to
-
Vonage Sender Number:
- Go to
Numbers > Your numbers
in the Vonage Dashboard. - Ensure you have a number with SMS capability in the country you intend to send from/to. If not, buy one using the
Buy numbers
tab. - Copy the full number (including country code, e.g.,
12015550123
) intoVONAGE_FROM_NUMBER
in your.env
file. - Important (US Traffic): For sending Application-to-Person (A2P) SMS in the US, this number must be associated with a registered 10DLC campaign. See Section 11.
- Go to
Environment Variable Summary:
VONAGE_API_KEY
: Your main account API key.VONAGE_API_SECRET
: Your main account API secret.VONAGE_APPLICATION_ID
: ID of the Vonage Application with Messages capability enabled. Used for authentication with the private key.VONAGE_PRIVATE_KEY_PATH
: Relative path from the project root to your downloadedprivate.key
file.VONAGE_FROM_NUMBER
: The Vonage virtual number (in E.164 format preferable, e.g.,14155552671
) used as the sender ID. Must be SMS-capable and potentially 10DLC registered.
5. Implementing error handling, logging, and retry mechanisms
Robust error handling is critical for a bulk messaging system.
-
Error Handling Strategy:
- Specific Errors: Catch errors during individual
vonage.messages.send()
calls within the loop (broadcastService.js
). Log the specific recipient and the error details provided by the Vonage SDK (err.response.data
). - General Errors: Catch errors in the API route handler (
server.js
) for issues like invalid input or unexpected failures in the service layer. Return appropriate HTTP status codes (400 for bad input, 500 for server errors). - Configuration Errors: Validate essential environment variables on startup (
vonageClient.js
,server.js
) and exit gracefully if missing.
- Specific Errors: Catch errors during individual
-
Logging:
- Basic Logging: We've used
console.log
,console.warn
, andconsole.error
. This is suitable for development but insufficient for production. - Production Logging: Use a dedicated logging library like Winston or Pino.
- Configure structured logging (JSON format) for easier parsing by log analysis tools.
- Set appropriate log levels (e.g.,
info
,warn
,error
). - Log key information: timestamp, log level, message, recipient number (if applicable), message UUID (if successful), error details.
- Direct logs to files or a centralized logging service (e.g., Datadog, Logstash, CloudWatch Logs).
(Conceptual Winston Example - not fully implemented here):
// conceptual logger setup const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.Console({ format: winston.format.simple() }), // Simple for console // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }) ], }); // Usage in broadcastService.js catch block: // logger.error('Failed to send SMS', { recipient: recipientNumber, error: errorDetail });
- Basic Logging: We've used
-
Retry Mechanisms:
- Why Retry? Transient network issues or temporary rate limiting (HTTP 429) can cause failures that might succeed on retry.
- Strategy: Implement exponential backoff. Wait increasingly longer intervals between retries (e.g., 1s, 2s, 4s, 8s...).
- Implementation:
- Manual: Write a helper function that wraps the
vonage.messages.send
call within a loop, catching specific retryable errors (like 429 or network errors) and usingsetTimeout
with increasing delays. - Libraries: Use libraries like
async-retry
orp-retry
to simplify retry logic.
- Manual: Write a helper function that wraps the
(Conceptual
async-retry
Example - needs installationnpm install async-retry
):// conceptual retry in broadcastService.js loop const retry = require('async-retry'); // Inside the for loop: try { await retry(async bail => { // bail is a function to stop retrying for non-recoverable errors console.log(`Attempting to send SMS to ${recipientNumber}...`); const resp = await vonage.messages.send({ /* ... params ... */ }); console.log(` Message submitted to ${recipientNumber}, UUID: ${resp.message_uuid}`); results.push({ to: recipientNumber, status: 'submitted', message_uuid: resp.message_uuid }); }, { retries: 3, // Number of retries minTimeout: 500, // Initial delay ms factor: 2, // Exponential backoff factor onRetry: (error, attempt) => { console.warn(`Retrying send to ${recipientNumber} (attempt ${attempt}) due to error: ${error?.response?.data?.title || error.message}`); // Don't retry on certain errors like invalid number format, insufficient funds etc. if (error?.response?.status && ![429, 500, 502, 503, 504].includes(error.response.status)) { bail(new Error('Non-retryable error: ' + (error?.response?.data?.title || error.message))); } } }); } catch (err) { // This catch block now handles errors after all retries have failed console.error(` Failed to send SMS to ${recipientNumber} after retries:`, err?.response?.data || err.message || err); const errorDetail = err?.response?.data || { message: err.message }; results.push({ to: recipientNumber, status: 'failed', error: errorDetail }); }
-
Testing Error Scenarios:
- Provide invalid credentials in
.env
. - Send to deliberately malformed phone numbers.
- Send a very large list rapidly to trigger rate limits (HTTP 429).
- Temporarily disable network connectivity.
- (If using webhooks) Send messages and then stop the local server to simulate webhook failures.
- Provide invalid credentials in
6. Creating a database schema and data layer (Conceptual)
While our example uses an in-memory array, a production system requires a database for persistence and tracking.
Why a Database?
- Recipient Management: Store large lists of recipients, potentially with segmentation or subscription status.
- Broadcast Job Tracking: Manage ongoing or scheduled broadcasts.
- Message Status Tracking: Store the
message_uuid
from Vonage and update the status (e.g.,submitted
,delivered
,failed
) via Status Webhooks (see Section 10). - Logging/Auditing: Persist detailed logs of sending activities.
Conceptual Schema (Example using Prisma-like syntax):
// conceptual schema.prisma
model Recipient {
id String @id @default(cuid())
phone String @unique // E.164 format
firstName String?
lastName String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Add relationships to groups/lists if needed
// groups RecipientGroup[]
}
model Broadcast {
id String @id @default(cuid())
name String? // Optional name for the broadcast job
message String
status String // e.g., pending, processing, completed, failed
scheduledAt DateTime? // For scheduled broadcasts
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages Message[] // Relation to individual messages sent
}
model Message {
id String @id @default(cuid())
vonageUUID String? @unique // message_uuid from Vonage API response
recipientPhone String // The phone number it was sent to
status String // e.g., submitted, delivered, failed, rejected, unknown
errorCode String? // Vonage error code if failed
errorReason String? // Vonage error description
price Decimal? // Cost reported by Vonage
currency String? // Currency reported by Vonage
sentAt DateTime @default(now())
statusUpdatedAt DateTime? // When the status was last updated via webhook
broadcast Broadcast @relation(fields: [broadcastId], references: [id])
broadcastId String
@@index([recipientPhone])
@@index([status])
@@index([broadcastId])
}
// Add models for recipient lists/groups if managing subscriptions
// model RecipientGroup { ... }
(Entity Relationship Diagram - Conceptual ASCII Art):
+-------------+ +-----------+ +---------+
| Recipient |-------| Message |-------| Broadcast |
|-------------| |-----------| |-----------|
| id (PK) | 1..* | id (PK) | 1..* | id (PK) |
| phone | | vonageUUID| | name |
| firstName | | status | | message |
| ... | | ... | | status |
| | | phone | | ... |
| | | bcastId(FK)| | |
+-------------+ +-----------+ +---------+
|
| links to
v
Recipient (implicitly via phone, or explicitly via recipientId FK)
Implementation:
- Choose an ORM: Prisma, Sequelize, or TypeORM are popular choices for Node.js.
- Define Schema: Use the ORM's schema definition language (like the Prisma example above).
- Migrations: Use the ORM's migration tools (
prisma migrate dev
,sequelize db:migrate
) to create and update the database tables safely. - Data Access Layer: Create functions (e.g.,
findRecipientByPhone
,createMessageRecord
,updateMessageStatusByUUID
) to interact with the database via the ORM, encapsulating database logic. - Integration: Modify
broadcastService.js
to fetch recipients from the DB andserver.js
to createBroadcast
andMessage
records. Implement webhook handlers (Section 10) to updateMessage
status.
Performance/Scale:
- Indexing: Add database indexes (
@@index
in Prisma) to frequently queried columns (vonageUUID
,status
,recipientPhone
,broadcastId
). - Batching: When creating many
Message
records, use batch insertion methods provided by the ORM if available. - Connection Pooling: Ensure your ORM and database driver use connection pooling effectively.
7. Adding security features
Protecting your application and user data is paramount.
-
Input Validation and Sanitization:
- API Layer: We implemented basic validation in
server.js
. Use more robust libraries likeexpress-validator
for complex validation rules (e.g., checking phone number formats rigorously, message length). - Sanitization: Ensure data stored or displayed (if applicable) is sanitized to prevent Cross-Site Scripting (XSS) if ever rendered in HTML. While less critical for an SMS API, it's good practice.
- API Layer: We implemented basic validation in
-
Authentication and Authorization:
- API Endpoint: The
/broadcast
endpoint is currently open. Secure it!- API Keys: Generate unique API keys for clients. Require clients to send the key in a header (e.g.,
X-API-Key
). Validate the key on the server against a stored list/database. - JWT (JSON Web Tokens): Implement user authentication if different users trigger broadcasts. Issue JWTs upon login, require them in the
Authorization: Bearer <token>
header, and verify the token signature and claims. - IP Whitelisting: Restrict access to known IP addresses if applicable.
- API Keys: Generate unique API keys for clients. Require clients to send the key in a header (e.g.,
- API Endpoint: The
-
Rate Limiting:
- Purpose: Prevent abuse and brute-force attacks, and help manage load.
- Implementation: Use middleware like
express-rate-limit
. Apply limits to sensitive endpoints like/broadcast
.
// In server.js const rateLimit = require('express-rate-limit'); const broadcastLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many broadcast requests created from this IP, please try again after 15 minutes' }); // Apply to the broadcast route app.post('/broadcast', broadcastLimiter, async (req, res) => { // ... existing route handler });
- Adjust
windowMs
andmax
based on expected usage and security needs. Consider using a persistent store like Redis for rate limiting across multiple server instances.