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