WebSockets & Real-Time Communication (Socket.io)
HTTP is a stateless, request-response protocol initiated solely by the client. For use cases requiring bidirectional, low-latency updates (chats, live metrics, collaboration tools), the WebSocket protocol (ws://, wss://) establishes a persistent, full-duplex TCP connection.
Socket.io builds on top of WebSockets by providing automatic reconnection, connection fallback (HTTP long-polling), packet buffering, acknowledgments, and horizontal scaling abstractions (rooms and namespaces).
Client ──▶ HTTP Upgrade Request ──▶ Server Handshake ──▶ Persistent Full-Duplex TCP Socket
Bash
npm install socket.io
Core Concepts: Namespaces, Rooms, and Broadcasting
- Namespaces (
io.of('/custom')): Multiplexes a single shared TCP connection into isolated logical channels with independent middleware, event handlers, and rooms. - Rooms (
socket.join('room-name')): Server-side-only subdivisions within a namespace. Sockets can join/leave rooms to receive targeted broadcasts without other clients receiving that traffic. - Broadcasting: Emitting an event to subsets of connected clients.
JavaScript
// server.js
import http from 'node:http';
import express from 'express';
import { Server } from 'socket.io';
import { verifyAccessToken } from './utils/jwt.js';
const app = express();
const server = http.createServer(app);
// Initialize Socket.io with strict CORS
const io = new Server(server, {
cors: {
origin: 'https://app.domain.com',
methods: ['GET', 'POST'],
credentials: true
},
pingTimeout: 20000,
pingInterval: 25000
});
// Authentication Middleware for WebSockets
io.use((socket, next) => {
const token = socket.handshake.auth?.token || socket.handshake.headers?.authorization?.split(' ')[1];
if (!token) {
return next(new Error('Authentication error: Missing token'));
}
try {
const user = verifyAccessToken(token);
socket.data.user = user; // Attach authenticated identity to socket instance
next();
} catch (err) {
next(new Error('Authentication error: Invalid or expired token'));
}
});
// Connection lifecycle & event dispatching
io.on('connection', (socket) => {
const userId = socket.data.user.sub;
console.log(`Socket connected: ${socket.id} (User: ${userId})`);
// Auto-join personal room for private notifications
socket.join(`user:${userId}`);
// Joining a group room
socket.on('room:join', (roomId) => {
socket.join(roomId);
// Broadcast to everyone ELSE in the room
socket.to(roomId).emit('user:joined', { userId, timestamp: Date.now() });
});
// Bidirectional messaging with Acknowledgments
socket.on('message:send', async (payload, callback) => {
try {
const messageRecord = {
senderId: userId,
text: payload.text,
roomId: payload.roomId,
createdAt: new Date().toISOString()
};
// Broadcast message to everyone in the target room (including the sender)
io.in(payload.roomId).emit('message:received', messageRecord);
// Fire callback acknowledgment to the original sender
callback({ status: 'delivered', id: 'msg_abc123' });
} catch (err) {
callback({ status: 'error', message: err.message });
}
});
// Leaving a room
socket.on('room:leave', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('user:left', { userId });
});
// Handle client disconnection
socket.on('disconnect', (reason) => {
console.log(`Socket ${socket.id} disconnected: ${reason}`);
});
});
server.listen(3000, () => console.log('Realtime server running on :3000'));
Event Emission Cheat Sheet
JavaScript
// Send to the current socket only
socket.emit('event', data);
// Send to all connected clients EXCEPT the sender
socket.broadcast.emit('event', data);
// Send to all clients in 'roomA' EXCEPT the sender
socket.to('roomA').emit('event', data);
// Send to all clients in 'roomA' INCLUDING the sender
io.in('roomA').emit('event', data);
// Send to a single targeted user by their personal room
io.to(`user:${targetUserId}`).emit('notification', { alert: 'New message' });
// Send to everyone connected to the entire server
io.emit('system:alert', { message: 'Server restarting in 5m' });
Horizontal Scaling with Redis Streams Adapter
Because WebSockets are stateful, running multiple Node.js instances behind a load balancer will isolate sockets to individual processes. Use the @socket.io/redis-adapter to publish and synchronize events across instances through Redis:
Bash
npm install @socket.io/redis-adapter ioredis
JavaScript
import { createAdapter } from '@socket.io/redis-adapter';
import Redis from 'ioredis';
const pubClient = new Redis(process.env.REDIS_URL);
const subClient = pubClient.duplicate();
// Distribute socket events across any number of clustered Node nodes
io.adapter(createAdapter(pubClient, subClient));
Background Jobs & Message Queues
Running long-running or CPU-heavy tasks (video transcoding, PDF generation, CSV imports, batch emails) synchronously inside an HTTP handler ties up the main thread and causes client timeouts.
Offload these operations to background worker queues.
HTTP POST /reports ──▶ [ Producer ] ──▶ BullMQ Job pushed to Redis ──▶ 202 Accepted
│
[ Background Worker Process ] ◀── Picks up job from Redis Queue ◀──────────────┘
1. BullMQ (Redis-Backed Distributed Queue)
BullMQ is the NodeJS standard for message queues, handling concurrency, exponential backoff retries, delayed jobs, parent-child flows, and metrics.
Bash
npm install bullmq ioredis
Step 1: Redis Connection & Queue Producer
JavaScript
// queues/email.queue.js
import { Queue } from 'bullmq';
const connection = {
host: process.env.REDIS_HOST || '127.0.0.1',
port: Number(process.env.REDIS_PORT) || 6379
};
export const emailQueue = new Queue('email-delivery', {
connection,
defaultJobOptions: {
attempts: 4, // Retry up to 4 times on failure
backoff: {
type: 'exponential',
delay: 2000 // 2s, 4s, 8s, 16s...
},
removeOnComplete: 1000, // Keep last 1,000 completed jobs in Redis for audit
removeOnFail: 5000 // Keep failed jobs for inspection
}
});
// Adding jobs to the queue from an Express controller
export async function scheduleWelcomeEmail(user) {
return await emailQueue.add(
'send-welcome',
{ userId: user.id, email: user.email, name: user.name },
{
priority: 2, // 1 is highest priority
delay: 5000 // Deliver 5 seconds after invocation
}
);
}
Step 2: Dedicated Worker Processor
Run workers in a separate process or container so heavy compute operations never interfere with HTTP traffic.
JavaScript
// workers/email.worker.js
import { Worker } from 'bullmq';
const connection = {
host: process.env.REDIS_HOST || '127.0.0.1',
port: Number(process.env.REDIS_PORT) || 6379
};
export const emailWorker = new Worker(
'email-delivery',
async (job) => {
console.log(`Processing job ${job.id} of type ${job.name}...`);
switch (job.name) {
case 'send-welcome': {
const { email, name } = job.data;
// Update job progress (0% - 100%) for UI trackers
await job.updateProgress(25);
// Execute the external operation
await sendEmailViaProvider({ to: email, template: 'welcome', context: { name } });
await job.updateProgress(100);
return { delivered: true, sentAt: new Date().toISOString() };
}
default:
throw new Error(`Unhandled job type: ${job.name}`);
}
},
{
connection,
concurrency: 5 // Process up to 5 jobs concurrently per worker instance
}
);
emailWorker.on('completed', (job, returnvalue) => {
console.log(`Job ${job.id} completed. Result:`, returnvalue);
});
emailWorker.on('failed', (job, err) => {
console.error(`Job ${job.id} failed after ${job.attemptsMade} attempts: ${err.message}`);
});
2. In-Process Cron Jobs (node-cron)
For simple, single-instance recurring scheduled tasks (e.g., nightly cache purges, daily report aggregation), node-cron provides a straightforward cron syntax parser.
Bash
npm install node-cron
JavaScript
// jobs/scheduler.js
import cron from 'node-cron';
// Cron Syntax: [second] [minute] [hour] [day-of-month] [month] [day-of-week]
// Runs every night at exactly 02:00 AM (server local time)
cron.schedule('0 0 2 * * *', async () => {
console.log('Running daily database backup and orphan file cleanup...');
try {
await purgeExpiredSessions();
console.log('Maintenance finished successfully');
} catch (err) {
console.error('Scheduled task failed:', err);
}
}, {
scheduled: true,
timezone: 'UTC' // Always lock cron execution to UTC
});
Warning on Clustered Deployments: If running multiple replicas of your app behind a load balancer, standard
node-cronschedules execute on every single running instance, causing duplicate executions. For multi-replica setups, use BullMQ Repeatable Jobs, which use Redis to enforce single execution across clusters:JavaScript
await queue.add('nightly-sync', {}, { repeat: { pattern: '0 2 * * *' } });
Email & Transactional Notifications
Transactional emails (password resets, invoice PDFs, magic links) are critical delivery pathways. Node.js applications use SMTP via Nodemailer or modern HTTP-based REST APIs (Resend, SendGrid, Postmark).
Method 1: Nodemailer (SMTP Engine)
Nodemailer connects directly to an arbitrary SMTP server (AWS SES, Mailgun, or local Postfix).
Bash
npm install nodemailer
JavaScript
// services/nodemailer.service.js
import nodemailer from 'nodemailer';
// Create persistent transporter pool
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: false, // true for 465, false for 587 (STARTTLS)
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
},
pool: true, // Keep connections alive to reuse TCP sockets
maxConnections: 5
});
export async function sendPasswordResetEmail({ to, token }) {
const resetLink = `https://app.domain.com/reset-password?token=${token}`;
const mailOptions = {
from: '"Security Team" <security@domain.com>',
to,
subject: 'Action Required: Reset Your Password',
text: `Reset your password by opening this URL: ${resetLink}`,
html: `
<div style="font-family: sans-serif; padding: 20px;">
<h2>Password Reset Request</h2>
<p>Click the link below to set a new password. This link expires in 15 minutes.</p>
<a href="${resetLink}" style="display:inline-block;padding:10px 20px;background:#0070f3;color:#fff;text-decoration:none;border-radius:5px;">Reset Password</a>
</div>
`
};
const info = await transporter.sendMail(mailOptions);
return { messageId: info.messageId };
}
Method 2: Modern HTTP APIs (Resend SDK)
Direct SMTP can be slow, vulnerable to firewall blocking, and requires maintaining persistent connections. Modern platforms use transactional HTTP APIs with lower overhead and built-in React template rendering.
Bash
npm install resend
JavaScript
// services/resend.service.js
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendInvoiceEmail({ to, invoiceId, pdfBuffer }) {
const { data, error } = await resend.emails.send({
from: 'billing@domain.com',
to: [to],
subject: `Invoice #${invoiceId} is available`,
html: '<p>Attached is your monthly billing statement. Thank you for your business!</p>',
attachments: [
{
filename: `invoice-${invoiceId}.pdf`,
content: pdfBuffer // Pass raw binary Buffer directly without writing to disk
}
],
tags: [
{ name: 'category', value: 'invoicing' }
]
});
if (error) {
throw new Error(`Email delivery failed: ${error.message}`);
}
return data; // { id: '49af6ee4-4806-44c6-8d8d-29406085ff87' }
}
Architectural Pattern: Combining WebSockets, Queues, and Email
1. Client POST /api/reports/export
│
├──▶ Controller pushes 'generate-pdf' task to BullMQ
└──▶ Immediately replies: 202 Accepted { jobId: 'job_456' }
2. BullMQ Worker picks up 'generate-pdf'
│
├──▶ Compiles heavy dynamic report
├──▶ Sends email with file attachment via Resend
└──▶ Emits completion to Redis PubSub
3. Socket.io Cluster receives Redis PubSub
│
└──▶ Dispatches socket event to target user room:
io.to(`user:${userId}`).emit('report:ready', { downloadUrl: '...' });
