Performancemedium

Implement client-side rate limiting for an action

Implement a token-bucket rate limiter in JS that throttles a function (e.g. log forwarder) to N calls per interval, queueing or dropping overflow.

Asked at CrowdStrike, Stripe

#rate limiting#token bucket#performance#algorithms

Answer

// Token bucket: capacity tokens, refilled at refillPerMs rate.
// Each call costs 1 token. If no token, either drop or queue.

export function createTokenBucket({
  capacity,
  refillPerSecond,
  mode = 'queue', // 'queue' | 'drop'
}) {
  let tokens = capacity
  let lastRefill = Date.now()
  const queue = []

  function refill() {
    const now = Date.now()
    const elapsed = (now - lastRefill) / 1000
    tokens = Math.min(capacity, tokens + elapsed * refillPerSecond)
    lastRefill = now
  }

  function tryDrain() {
    refill()
    while (queue.length && tokens >= 1) {
      tokens -= 1
      const { fn, resolve, reject } = queue.shift()
      Promise.resolve()
        .then(fn)
        .then(resolve, reject)
    }
    if (queue.length) {
      const waitMs = ((1 - tokens) / refillPerSecond) * 1000
      setTimeout(tryDrain, Math.max(10, waitMs))
    }
  }

  return function schedule(fn) {
    return new Promise((resolve, reject) => {
      refill()
      if (tokens >= 1 && queue.length === 0) {
        tokens -= 1
        Promise.resolve().then(fn).then(resolve, reject)
        return
      }
      if (mode === 'drop') {
        reject(new Error('rate-limited'))
        return
      }
      queue.push({ fn, resolve, reject })
      tryDrain()
    })
  }
}

// Usage: forward at most 5 logs/sec, queue the rest
const sendLog = createTokenBucket({ capacity: 10, refillPerSecond: 5 })

for (const event of events) {
  sendLog(() => fetch('/log', { method: 'POST', body: JSON.stringify(event) }))
    .catch((e) => console.warn('dropped', e))
}

// Talking points:
// - Token bucket allows bursts up to capacity (good for spiky log streams).
// - Leaky bucket (fixed rate, no bursts) is stricter — pick by use case.
// - Always pair with server-side rate limiting; client-side is a politeness layer.
// - For multi-tab apps, coordinate via BroadcastChannel or a SharedWorker so
//   tabs don't each get their own quota.

Practise more Performance questions →