Learn WebSocket - Rate Limiting & Throttling
Episode 13 of 34

Learn WebSocket - Rate Limiting & Throttling

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Why Rate Limiting

The Danger of No Limits

Without rate limiting, the server is exposed to various problems:

  • Abuse: automated scripts flooding the server with spam messages.
  • Resource exhaustion: CPU and memory spent processing cheap messages.
  • DDoS: mass connections filling the server's capacity.
  • Cost explosion: bandwidth and compute costs ballooning without control.

Rate Limiting Algorithms

Token Bucket

JSSimple token bucket implementation
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.

Sliding Window

A common alternative is a window that slides based on time.

Install a rate limit middleware
bun add express-rate-limit

The 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.

Server Implementation

Per-User and Per-Connection Limits

Rate limiting is applied at two levels: per connection and per authenticated user.

JSRate limit on the message handler
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.

Redis for Multiple Servers

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.

JSRate limit counter in Redis
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.

Client-Side Throttling

Debounce and Batching

The server is not the only place to slow things down. A good client holds back excessive user input.

JSDebouncing 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.

A Bounded Queue

When offline, sent messages must be queued, but the queue must not grow without limit.

JSQueue with a maximum size
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.

Backpressure Handling

A server receiving data faster than it can process it needs backpressure. The ws library provides bufferedAmount to detect it.

JSDetecting bufferedAmount
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.

Closing

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:

  • Rate limiting prevents abuse, resource exhaustion, and DDoS.
  • Token bucket allows small bursts with a stable average.
  • Limit per connection and per authenticated user.
  • Redis keeps counters consistent across many server instances.
  • Debounce and bounded queues honor the server from the client side.
  • bufferedAmount detects backpressure before memory fills up.

In the next episode we cover message queuing & reliability: delivery guarantees, acknowledgement, message queues, and strategies for keeping order and preventing data loss.

Learn WebSocket - Rate Limiting & Throttling | Learn WebSocket