Rate Limiting Algorithms

distributed-systems
algorithms
Published

March 22, 2026

Show rate limiter simulation code
{
  const dt = 0.05;
  const duration = 30;
  const steps = Math.floor(duration / dt);

  // Generate traffic: silence, a single burst, then silence
  let times = [], input = [];
  for (let i = 0; i < steps; i++) {
    let t = i * dt;
    times.push(t);
    let rate;
    if (t < 5) rate = 0;
    else if (t < 8) rate = 80;
    else rate = 0;
    input.push(rate);
  }

  // Token Bucket: refills tokens at constant rate, spends one per request.
  // Stored tokens allow short bursts through; excess is queued and
  // released as tokens refill — no traffic is dropped.
  let tbTokens = config.tbCapacity;
  let tbQueue = 0;
  let tbOut = [];
  for (let i = 0; i < steps; i++) {
    let arriving = input[i] * dt;
    tbTokens = Math.min(config.tbCapacity, tbTokens + config.tbFillRate * dt);
    let fromQueue = Math.min(tbQueue, tbTokens);
    tbQueue -= fromQueue;
    tbTokens -= fromQueue;
    let passed = Math.min(arriving, tbTokens);
    tbTokens -= passed;
    tbQueue += (arriving - passed);
    tbOut.push((fromQueue + passed) / dt);
  }

  // Sliding Window: counts served requests in a rolling time window.
  // Serves up to the limit per window; excess is queued and released
  // as the window slides — no traffic is dropped.
  let swServed = [];
  let swQueue = 0;
  let swOut = [];
  for (let i = 0; i < steps; i++) {
    let arriving = input[i] * dt;
    let windowSteps = Math.floor(config.swWindow / dt);
    let inWindow = 0;
    for (let j = Math.max(0, i - windowSteps); j < i; j++) {
      inWindow += swServed[j];
    }
    let canServe = Math.max(0, config.swLimit - inWindow);
    let fromQueue = Math.min(swQueue, canServe);
    swQueue -= fromQueue;
    canServe -= fromQueue;
    let passed = Math.min(arriving, canServe);
    swQueue += (arriving - passed);
    let totalServed = fromQueue + passed;
    swServed.push(totalServed);
    swOut.push(totalServed / dt);
  }

  // Leaky Bucket: all traffic enters an unbounded queue that drains at
  // a constant rate — no traffic is dropped.
  let lbQueue = 0;
  let lbOut = [];
  for (let i = 0; i < steps; i++) {
    let arriving = input[i] * dt;
    lbQueue += arriving;
    let drained = Math.min(lbQueue, config.lbDrainRate * dt);
    lbQueue -= drained;
    lbOut.push(drained / dt);
  }

  // All series on a single chart
  let data = [];
  for (let i = 0; i < steps; i++) {
    data.push({t: times[i], rate: input[i], type: "Input Rate"});
    data.push({t: times[i], rate: tbOut[i], type: "Token Bucket"});
    data.push({t: times[i], rate: swOut[i], type: "Sliding Window"});
    data.push({t: times[i], rate: lbOut[i], type: "Leaky Bucket"});
  }

  return Plot.plot({
    marks: [
      Plot.areaY(data.filter(d => d.type === "Input Rate"), {
        x: "t", y: "rate", fill: "#6366f1", fillOpacity: 0.08
      }),
      Plot.lineY(data.filter(d => d.type === "Input Rate"), {
        x: "t", y: "rate", stroke: "#6366f1", strokeWidth: 2
      }),
      Plot.lineY(data.filter(d => d.type !== "Input Rate"), {
        x: "t", y: "rate", stroke: "type", strokeWidth: 2,
        strokeOpacity: 0.6, strokeDasharray: "6 3"
      }),
      Plot.ruleY([0])
    ],
    color: {
      domain: ["Input Rate", "Token Bucket", "Sliding Window", "Leaky Bucket"],
      range: ["#6366f1", "#10b981", "#f59e0b", "#ef4444"],
      legend: true
    },
    x: {label: "Time (arbitrary unit)"},
    y: {label: "Rate (arbitrary unit)", domain: [0, 100]},
    width: width,
    height: 400,
    marginLeft: 50
  });
}