frontend
Function Call Limit (Rate Limiting) in JavaScript
January 24, 2026
Function Call Limit (Rate Limiting) in JavaScript
Overview
Function call limiting (rate limiting) is a technique to restrict how many times a function can be called within a specified time period. This is essential for preventing abuse, managing API rate limits, controlling resource usage, and protecting against excessive function invocations.
Basic Implementation
/**
* Implement a function rateLimit which takes a function, a limit, and a time.
* The function should be called no more than limit times in time milliseconds.
*/
function funcCallLimit(func, limit, time) {
let calls = [];
return function (...args) {
const now = Date.now();
// Remove timestamps older than `time` ms
calls = calls.filter((timestamp) => now - timestamp < time);
if (calls.length < limit) {
calls.push(now);
func.apply(this, args);
}
// Else: silently ignore the call
};
}
// Usage
const funcCall = funcCallLimit(console.log, 3, 1000); // 3 calls per 1 second
funcCall("Hello");
funcCall("World");
funcCall("!");
funcCall("not called"); // This call is ignored
Advanced Implementation with Return Value
function rateLimit(func, limit, time) {
let calls = [];
return function (...args) {
const now = Date.now();
// Remove old timestamps
calls = calls.filter((timestamp) => now - timestamp < time);
if (calls.length < limit) {
calls.push(now);
return func.apply(this, args);
}
// Return error or throw when limit exceeded
throw new Error('Rate limit exceeded');
};
}
Implementation with Queue
function rateLimitWithQueue(func, limit, time) {
let calls = [];
let queue = [];
function processQueue() {
const now = Date.now();
calls = calls.filter((timestamp) => now - timestamp < time);
while (queue.length > 0 && calls.length < limit) {
const { args, resolve, reject } = queue.shift();
calls.push(Date.now());
try {
const result = func.apply(this, args);
resolve(result);
} catch (error) {
reject(error);
}
}
}
return function (...args) {
return new Promise((resolve, reject) => {
queue.push({ args, resolve, reject });
processQueue();
// Process queue periodically
if (queue.length > 0) {
setTimeout(processQueue, time / limit);
}
});
};
}
Sliding Window Rate Limiter
class SlidingWindowRateLimiter {
constructor(limit, windowMs) {
this.limit = limit;
this.windowMs = windowMs;
this.calls = [];
}
canCall() {
const now = Date.now();
// Remove calls outside the window
this.calls = this.calls.filter(
(timestamp) => now - timestamp < this.windowMs
);
if (this.calls.length < this.limit) {
this.calls.push(now);
return true;
}
return false;
}
getTimeUntilNextCall() {
if (this.calls.length < this.limit) {
return 0;
}
const oldestCall = this.calls[0];
const timeSinceOldest = Date.now() - oldestCall;
return Math.max(0, this.windowMs - timeSinceOldest);
}
}
function rateLimit(func, limit, time) {
const limiter = new SlidingWindowRateLimiter(limit, time);
return function (...args) {
if (limiter.canCall()) {
return func.apply(this, args);
} else {
const waitTime = limiter.getTimeUntilNextCall();
throw new Error(`Rate limit exceeded. Try again in ${waitTime}ms`);
}
};
}
Token Bucket Algorithm
class TokenBucket {
constructor(capacity, refillRate, refillPeriod) {
this.capacity = capacity;
this.tokens = capacity;
this.refillRate = refillRate;
this.refillPeriod = refillPeriod;
this.lastRefill = Date.now();
}
consume(tokens = 1) {
this.refill();
if (this.tokens >= tokens) {
this.tokens -= tokens;
return true;
}
return false;
}
refill() {
const now = Date.now();
const timePassed = now - this.lastRefill;
const tokensToAdd = Math.floor(
(timePassed / this.refillPeriod) * this.refillRate
);
if (tokensToAdd > 0) {
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
getTimeUntilNextToken() {
this.refill();
if (this.tokens >= 1) {
return 0;
}
const tokensNeeded = 1 - this.tokens;
return Math.ceil((tokensNeeded / this.refillRate) * this.refillPeriod);
}
}
function rateLimitWithTokenBucket(func, limit, time) {
const bucket = new TokenBucket(limit, limit, time);
return function (...args) {
if (bucket.consume()) {
return func.apply(this, args);
} else {
const waitTime = bucket.getTimeUntilNextToken();
throw new Error(`Rate limit exceeded. Try again in ${waitTime}ms`);
}
};
}
Use Cases
1. API Rate Limiting
const apiCall = rateLimit(
async (url) => {
const response = await fetch(url);
return response.json();
},
10, // 10 calls
60000 // per minute
);
// Usage
try {
const data = await apiCall('/api/data');
} catch (error) {
console.error('Rate limit exceeded');
}
2. Button Click Throttling
const handleClick = rateLimit(
() => {
console.log('Button clicked');
// Perform action
},
1, // 1 click
1000 // per second
);
button.addEventListener('click', handleClick);
3. Search Input Debouncing with Rate Limit
function debounceWithRateLimit(func, debounceMs, limit, limitWindowMs) {
let timeoutId;
const rateLimited = rateLimit(func, limit, limitWindowMs);
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
try {
rateLimited.apply(this, args);
} catch (error) {
console.error('Rate limit exceeded');
}
}, debounceMs);
};
}
4. File Upload Rate Limiting
const uploadFile = rateLimit(
async (file) => {
const formData = new FormData();
formData.append('file', file);
await fetch('/api/upload', {
method: 'POST',
body: formData
});
},
5, // 5 uploads
60000 // per minute
);
Advanced: Per-User Rate Limiting
class PerUserRateLimiter {
constructor(limit, windowMs) {
this.limit = limit;
this.windowMs = windowMs;
this.users = new Map();
}
canCall(userId) {
const now = Date.now();
if (!this.users.has(userId)) {
this.users.set(userId, []);
}
const calls = this.users.get(userId);
const validCalls = calls.filter(
(timestamp) => now - timestamp < this.windowMs
);
if (validCalls.length < this.limit) {
validCalls.push(now);
this.users.set(userId, validCalls);
return true;
}
return false;
}
reset(userId) {
this.users.delete(userId);
}
}
function rateLimitPerUser(func, limit, time) {
const limiter = new PerUserRateLimiter(limit, time);
return function (userId, ...args) {
if (limiter.canCall(userId)) {
return func.apply(this, args);
} else {
throw new Error('Rate limit exceeded for user');
}
};
}
Best Practices
- Choose Appropriate Limits: Balance between too restrictive and too permissive
- Handle Errors Gracefully: Provide clear error messages when limits are exceeded
- Consider Different Strategies: Sliding window vs token bucket based on use case
- Monitor Usage: Track rate limit hits for optimization
- Provide Feedback: Inform users when they hit rate limits
- Clean Up: Remove old timestamps to prevent memory leaks
- Per-User Limits: Consider per-user limits for multi-user systems
Comparison of Algorithms
Fixed Window
- Simple to implement
- Can allow bursts at window boundaries
- Less accurate
Sliding Window
- More accurate
- Prevents bursts
- Slightly more complex
Token Bucket
- Allows bursts up to capacity
- Smooth refill rate
- More complex implementation
Real-World Example
class RateLimiter {
constructor(options = {}) {
this.limit = options.limit || 10;
this.windowMs = options.windowMs || 60000;
this.strategy = options.strategy || 'sliding';
this.calls = [];
}
canCall() {
const now = Date.now();
if (this.strategy === 'sliding') {
this.calls = this.calls.filter(
(timestamp) => now - timestamp < this.windowMs
);
if (this.calls.length < this.limit) {
this.calls.push(now);
return true;
}
return false;
}
// Fixed window implementation
const windowStart = now - this.windowMs;
const recentCalls = this.calls.filter(
(timestamp) => timestamp > windowStart
);
if (recentCalls.length < this.limit) {
this.calls.push(now);
return true;
}
return false;
}
getRemainingCalls() {
const now = Date.now();
this.calls = this.calls.filter(
(timestamp) => now - timestamp < this.windowMs
);
return Math.max(0, this.limit - this.calls.length);
}
}
// Usage
const limiter = new RateLimiter({ limit: 5, windowMs: 1000 });
const limitedFunc = (...args) => {
if (limiter.canCall()) {
return originalFunc(...args);
}
throw new Error('Rate limit exceeded');
};