This episode covers making WebSocket applications faster: connection and payload optimization, Node.js clustering with workers, event loop monitoring, client debouncing, and network and TLS optimization.

The application works, but is it fast enough? In this episode we measure and fix: pushing latency down, reducing bandwidth, using every CPU core, and ensuring the Node.js event loop never stalls.
Episode 17 covers performance optimization from four sides: connections, messages, the server, and the client. You will learn techniques that can be applied immediately, from compression to clustering.
A healthy WebSocket connection needs tuning so it is neither wasteful nor dies in vain.
sysctl net.ipv4.tcp_keepalive_time
sysctl net.ipv4.tcp_keepalive_intvl
sysctl net.ipv4.tcp_keepalive_probesThe tcp_keepalive_time parameter decides when the kernel starts sending keep-alive packets. For long-lived connections like WebSocket, a sensible value (for example 60 seconds) keeps NATs and load balancers from evicting idle connections.
A TCP buffer that is too small holds back throughput; one too large holds memory.
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: true,
maxPayload: 64 * 1024,
});maxPayload: 64 * 1024 limits the message size so a single giant message cannot exhaust memory. This limit also prevents attacks that send very large payloads.
Message size is bandwidth and latency. Three ways to reduce it:
const pesan = {
t: "chat", // singkatan tipe
r: "node", // singkatan room
m: "Halo!", // isi pesan
};
ws.send(JSON.stringify(pesan));type becomes t, message becomes m. Saves 30-50 percent.Sending a hundred small messages costs more than one large message.
const antrean = [];
setInterval(() => {
if (antrean.length === 0) return;
ws.send(JSON.stringify({
type: "batch",
items: antrean.splice(0, antrean.length),
}));
}, 100);antrean.splice(0, antrean.length) takes all the updates accumulated in 100 ms. A mouse position updated 60 times per second only needs to be sent 10 times per second in one batch.
Node.js runs on a single CPU core. Clustering uses every core.
const cluster = require("cluster");
const os = require("os");
if (cluster.isPrimary) {
const jumlah = os.cpus().length;
for (let i = 0; i < jumlah; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log("worker mati, fork ulang:", worker.process.pid);
cluster.fork();
});
} else {
const { WebSocketServer } = require("ws");
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => ws.send("terhubung ke worker " + process.pid));
}cluster.isPrimary distinguishes the primary process from the workers. Each worker serves its own connections on the same port. The primary process re-forks dead workers to keep availability high.
A blocked event loop slows down every connection. Measure its delay regularly.
const mulai = Date.now();
setInterval(() => {
const delay = Date.now() - mulai;
if (delay > 100) {
console.warn("event loop tersumbat:", delay, "ms");
}
mulai = Date.now();
}, 100);Date.now() - mulai measures the difference between the scheduled interval and the actual run. A delay above 100 ms signals a synchronous task that is too heavy — usually a giant JSON.parse or a blocking operation — and it should be moved to a worker thread.
A frugal client makes the server lighter and the experience smoother.
let lastSend = 0;
canvas.onmousemove = (e) => {
const kini = Date.now();
if (kini - lastSend < 50) return;
lastSend = kini;
ws.send(JSON.stringify({ type: "pointer", x: e.x, y: e.y }));
};kini - lastSend < 50 caps sending at 20 times per second. For long message lists, virtual scrolling renders only the visible rows, so the browser does not freeze when thousands of messages arrive.
An optimized network cuts latency from the infrastructure side.
Keep in mind: the load balancer adds one hop. Measure end-to-end latency before and after adding infrastructure layers.
Episode 17 gave you performance glasses: every message byte, every synchronous task, and every network layer can become a bottleneck. Optimization starts with measurement, not guesses.
Key takeaways:
In the next episode we cover monitoring & observability: connection and message metrics, logging, Prometheus and Grafana integration, distributed tracing with OpenTelemetry, and alerting.