This episode covers protecting the server from abuse: rate limiting algorithms such as token bucket and sliding window, per-user and per-connection limits, client-side throttling, and backpressure handling.

A single misbehaving client sending a thousand messages per second can cripple a server serving thousands of normal users. Rate limiting is the fence that restricts how many requests may be made within a time period.
Episode 13 covers rate limiting: limiting the frequency of actions, and throttling: slowing down data delivery. You will learn the classic algorithms such as token bucket, their implementation on the Node.js server, and how a client honors the limits the server gives it.
Without rate limiting, the server is exposed to various problems:
const state = new Map();
function izinkan(userId, kapasitas = 10, perDetik = 1) {
const sekarang = Date.now() / 1000;
const s = state.get(userId) || { token: kapasitas, waktu: sekarang };
s.token = Math.min(
kapasitas,
s.token + (sekarang - s.waktu) * perDetik
);
s.waktu = sekarang;
if (s.token < 1) {
return false;
}
s.token -= 1;
state.set(userId, s);
return true;
}izinkan(userId) returns true if tokens remain, false if the client is limited. Tokens accumulate over time at the rate perDetik and are capped at the maximum kapasitas.
A common alternative is a window that slides based on time.
bun add express-rate-limitThe express-rate-limit library implements a sliding window for REST. For WebSocket, you apply the same logic in the message handler, not in HTTP middleware.
Rate limiting is applied at two levels: per connection and per authenticated user.
wss.on("connection", (ws) => {
ws.limit = { jumlah: 0, mulai: Date.now() };
ws.on("message", (data) => {
const kini = Date.now();
if (kini - ws.limit.mulai > 10000) {
ws.limit = { jumlah: 0, mulai: kini };
}
ws.limit.jumlah += 1;
if (ws.limit.jumlah > 30) {
ws.close(1008, "terlalu banyak pesan");
return;
}
prosesPesan(data);
});
});A 10-second window with a maximum of 30 messages per connection above. After the limit is exceeded, ws.close(1008, "terlalu banyak pesan") terminates the connection with a policy violation reason.
When there is more than one server, an in-memory Map no longer suffices because each instance has its own count. Store the counter in Redis with a TTL.
const redis = require("redis");
const client = redis.createClient();
async function batasi(userId) {
const kunci = "rl:" + userId;
const jumlah = await client.incr(kunci);
if (jumlah === 1) {
await client.expire(kunci, 10);
}
return jumlah > 30;
}client.incr(kunci) increments an atomic counter, and client.expire(kunci, 10) clears it after 10 seconds. All server instances read the same counter, so limiting stays consistent across instances.
The server is not the only place to slow things down. A good client holds back excessive user input.
let timer;
input.oninput = () => {
clearTimeout(timer);
timer = setTimeout(() => {
ws.send(JSON.stringify({ type: "ketik", teks: input.value }));
}, 300);
};clearTimeout(timer) cancels the previous send, so only the last message within 300 ms is sent. Debounce is very useful for features such as typing status and live search.
When offline, sent messages must be queued, but the queue must not grow without limit.
const antrean = [];
function kirim(pesan) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(pesan));
} else {
if (antrean.length >= 50) {
antrean.shift();
}
antrean.push(pesan);
}
}antrean.shift() discards the oldest message when the queue exceeds 50 items. This prevents client memory from ballooning when the connection is dead for a long time.
A server receiving data faster than it can process it needs backpressure. The ws library provides bufferedAmount to detect it.
function kirimAman(ws, pesan) {
if (ws.bufferedAmount > 1024 * 1024) {
console.warn("antrean keluar penuh, menunda pengiriman");
return false;
}
ws.send(JSON.stringify(pesan));
return true;
}ws.bufferedAmount shows how many bytes have not yet been sent. If it exceeds the threshold, you can drop non-critical messages, inform the client, or reduce the data production rate.
Episode 13 gave you layered protection: the server limits with fair algorithms, counters stored to work across instances, and a client that restrains itself with debounce and bounded queues.
Key takeaways:
In the next episode we cover message queuing & reliability: delivery guarantees, acknowledgement, message queues, and strategies for keeping order and preventing data loss.