Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on API Security and Rate Limiting! ๐ In this guide, weโll explore how to protect your TypeScript APIs from abuse and ensure fair usage for all your users.
Youโll discover how rate limiting can transform your API from a vulnerable target ๐ฏ into a fortress ๐ฐ. Whether youโre building public APIs ๐, microservices ๐ฅ๏ธ, or web applications ๐ฑ, understanding rate limiting is essential for creating robust, scalable systems.
By the end of this tutorial, youโll feel confident implementing rate limiting in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Rate Limiting
๐ค What is Rate Limiting?
Rate limiting is like a bouncer at a popular club ๐บ. Think of it as a traffic controller ๐ฆ that ensures no single user can overwhelm your API with too many requests.
In TypeScript terms, rate limiting controls how many requests a client can make within a specific time window โฐ. This means you can:
- โจ Prevent API abuse and DDoS attacks
- ๐ Ensure fair resource distribution
- ๐ก๏ธ Protect backend services from overload
- ๐ฐ Control costs in cloud environments
๐ก Why Use Rate Limiting?
Hereโs why developers love rate limiting:
- Security Protection ๐: Stop brute force attacks
- Performance Stability ๐ป: Maintain consistent response times
- Cost Control ๐ต: Prevent unexpected cloud bills
- Fair Usage ๐ค: Ensure all users get access
Real-world example: Imagine a pizza delivery API ๐. Without rate limiting, one hungry customer could order 10,000 pizzas per second, crashing the system for everyone else!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example:
// ๐ Hello, Rate Limiting!
interface RateLimitRule {
windowMs: number; // โฐ Time window in milliseconds
maxRequests: number; // ๐ข Maximum requests allowed
message?: string; // ๐ฌ Custom error message
}
// ๐จ Creating a simple rate limiter
class SimpleRateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(private rule: RateLimitRule) {}
// ๐ฆ Check if request is allowed
isAllowed(clientId: string): boolean {
const now = Date.now();
const requests = this.requests.get(clientId) || [];
// ๐งน Clean old requests
const validRequests = requests.filter(
time => now - time < this.rule.windowMs
);
if (validRequests.length >= this.rule.maxRequests) {
console.log(`๐ซ Rate limit exceeded for ${clientId}`);
return false;
}
// โ
Add new request
validRequests.push(now);
this.requests.set(clientId, validRequests);
return true;
}
}
๐ก Explanation: This simple rate limiter tracks requests per client and blocks when limits are exceeded!
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Token Bucket Algorithm
class TokenBucket {
private tokens: number;
private lastRefill: number = Date.now();
constructor(
private capacity: number, // ๐ชฃ Bucket size
private refillRate: number // โก Tokens per second
) {
this.tokens = capacity;
}
// ๐ฏ Try to consume tokens
consume(count: number = 1): boolean {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true; // โ
Request allowed
}
return false; // โ Not enough tokens
}
// ๐ง Refill bucket
private refill(): void {
const now = Date.now();
const timePassed = (now - this.lastRefill) / 1000;
const tokensToAdd = timePassed * this.refillRate;
this.tokens = Math.min(
this.capacity,
this.tokens + tokensToAdd
);
this.lastRefill = now;
}
}
// ๐จ Pattern 2: Sliding Window
interface RequestLog {
timestamp: number;
weight?: number; // ๐๏ธ Optional request weight
}
// ๐ Pattern 3: Distributed Rate Limiting
interface DistributedLimiter {
checkLimit(key: string): Promise<boolean>;
increment(key: string): Promise<void>;
reset(key: string): Promise<void>;
}
๐ก Practical Examples
๐ Example 1: E-commerce API Protection
Letโs build something real:
// ๐๏ธ E-commerce API rate limiter
interface ApiEndpoint {
path: string;
rateLimit: RateLimitRule;
premium?: RateLimitRule; // ๐ Premium users get more!
}
class EcommerceRateLimiter {
private limiters = new Map<string, SimpleRateLimiter>();
constructor(private endpoints: ApiEndpoint[]) {
// ๐๏ธ Initialize limiters for each endpoint
endpoints.forEach(endpoint => {
this.limiters.set(
endpoint.path,
new SimpleRateLimiter(endpoint.rateLimit)
);
if (endpoint.premium) {
this.limiters.set(
`${endpoint.path}:premium`,
new SimpleRateLimiter(endpoint.premium)
);
}
});
}
// ๐ฆ Check rate limit
checkRequest(
path: string,
userId: string,
isPremium: boolean = false
): { allowed: boolean; retryAfter?: number } {
const limiterKey = isPremium ? `${path}:premium` : path;
const limiter = this.limiters.get(limiterKey);
if (!limiter) {
console.log(`โ ๏ธ No rate limit configured for ${path}`);
return { allowed: true };
}
const allowed = limiter.isAllowed(userId);
if (!allowed) {
// ๐ Calculate retry time
const rule = this.endpoints.find(e => e.path === path);
const retryAfter = rule ? Math.ceil(rule.rateLimit.windowMs / 1000) : 60;
return { allowed: false, retryAfter };
}
return { allowed: true };
}
}
// ๐ฎ Let's use it!
const apiLimiter = new EcommerceRateLimiter([
{
path: "/api/products",
rateLimit: { windowMs: 60000, maxRequests: 100 }, // ๐ฏ 100 req/min
premium: { windowMs: 60000, maxRequests: 1000 } // ๐ 1000 req/min
},
{
path: "/api/checkout",
rateLimit: { windowMs: 300000, maxRequests: 10 } // ๐ 10 req/5min
},
{
path: "/api/search",
rateLimit: { windowMs: 10000, maxRequests: 20 } // ๐ 20 req/10sec
}
]);
// ๐งช Test the limiter
console.log("๐งช Testing rate limiter...");
const result = apiLimiter.checkRequest("/api/products", "user123");
if (result.allowed) {
console.log("โ
Request allowed!");
} else {
console.log(`โ Rate limited! Retry after ${result.retryAfter}s`);
}
๐ฏ Try it yourself: Add a feature to temporarily ban users who repeatedly hit rate limits!
๐ฎ Example 2: Gaming API with Adaptive Limits
Letโs make it fun:
// ๐ Gaming API with dynamic rate limits
interface Player {
id: string;
level: number;
reputation: number; // ๐ 0-100
isPro: boolean;
}
interface GameAction {
type: "attack" | "trade" | "chat" | "move";
cost: number; // ๐ฐ Token cost
}
class GameRateLimiter {
private buckets = new Map<string, TokenBucket>();
// ๐ฏ Get bucket size based on player level
private getBucketSize(player: Player): number {
let baseSize = 100;
// ๐ Level bonus
baseSize += player.level * 10;
// ๐ Reputation bonus
baseSize += Math.floor(player.reputation / 10) * 5;
// ๐ Pro player bonus
if (player.isPro) {
baseSize *= 2;
}
return baseSize;
}
// โก Get refill rate
private getRefillRate(player: Player): number {
let baseRate = 10; // tokens per second
// ๐ Higher levels refill faster
baseRate += Math.floor(player.level / 10);
// ๐ Good reputation = faster refill
if (player.reputation > 80) {
baseRate *= 1.5;
}
return baseRate;
}
// ๐ฎ Check if action is allowed
canPerformAction(player: Player, action: GameAction): boolean {
const bucketKey = `${player.id}:${action.type}`;
// ๐ชฃ Get or create bucket
let bucket = this.buckets.get(bucketKey);
if (!bucket) {
bucket = new TokenBucket(
this.getBucketSize(player),
this.getRefillRate(player)
);
this.buckets.set(bucketKey, bucket);
}
// ๐ฐ Try to consume tokens
const allowed = bucket.consume(action.cost);
if (allowed) {
console.log(`โ
${player.id} performed ${action.type}! ๐ฎ`);
} else {
console.log(`โณ ${player.id} must wait to ${action.type}`);
}
return allowed;
}
// ๐ Grant bonus tokens
grantBonus(playerId: string, tokens: number): void {
console.log(`๐ Granting ${tokens} bonus tokens to ${playerId}!`);
// Implementation for bonus tokens
}
}
// ๐งช Test the game limiter
const gameLimiter = new GameRateLimiter();
const proPlayer: Player = {
id: "ProGamer123",
level: 50,
reputation: 95,
isPro: true
};
const newPlayer: Player = {
id: "Newbie456",
level: 1,
reputation: 50,
isPro: false
};
// ๐ฎ Simulate game actions
gameLimiter.canPerformAction(proPlayer, { type: "attack", cost: 10 });
gameLimiter.canPerformAction(newPlayer, { type: "chat", cost: 1 });
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Distributed Rate Limiting with Redis
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced distributed rate limiter
import { Redis } from 'ioredis'; // ๐ฆ Redis client
interface DistributedRateLimitOptions {
redis: Redis;
keyPrefix?: string;
algorithm: "sliding-window" | "token-bucket" | "fixed-window";
}
class DistributedRateLimiter {
constructor(
private options: DistributedRateLimitOptions,
private rule: RateLimitRule
) {}
// ๐ช Sliding window with Redis
async checkSlidingWindow(identifier: string): Promise<boolean> {
const key = `${this.options.keyPrefix}:${identifier}`;
const now = Date.now();
const windowStart = now - this.rule.windowMs;
// ๐๏ธ Remove old entries
await this.options.redis.zremrangebyscore(key, 0, windowStart);
// ๐ Count current requests
const count = await this.options.redis.zcard(key);
if (count >= this.rule.maxRequests) {
return false; // โ Limit exceeded
}
// โจ Add new request
await this.options.redis.zadd(key, now, `${now}-${Math.random()}`);
await this.options.redis.expire(key, Math.ceil(this.rule.windowMs / 1000));
return true; // โ
Request allowed
}
// ๐ญ Lua script for atomic operations
private readonly luaScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, current_time, current_time)
redis.call('EXPIRE', key, window)
return 1
else
return 0
end
`;
}
๐๏ธ Advanced Topic 2: Adaptive Rate Limiting
For the brave developers:
// ๐ Self-adjusting rate limiter
interface SystemMetrics {
cpuUsage: number; // ๐ 0-100%
memoryUsage: number; // ๐พ 0-100%
responseTime: number; // โฑ๏ธ milliseconds
errorRate: number; // โ 0-100%
}
class AdaptiveRateLimiter {
private baseLimit: number;
private currentLimit: number;
constructor(
baseLimit: number,
private metricsProvider: () => SystemMetrics
) {
this.baseLimit = baseLimit;
this.currentLimit = baseLimit;
}
// ๐ฏ Adjust limits based on system health
adjustLimits(): void {
const metrics = this.metricsProvider();
// ๐ Calculate health score (0-100)
const healthScore = this.calculateHealthScore(metrics);
// ๐จ Adjust limits dynamically
if (healthScore > 80) {
// ๐ System healthy - increase limits
this.currentLimit = Math.min(
this.baseLimit * 1.5,
this.currentLimit * 1.1
);
console.log(`๐ Increasing limit to ${this.currentLimit}`);
} else if (healthScore < 50) {
// ๐ด System stressed - decrease limits
this.currentLimit = Math.max(
this.baseLimit * 0.5,
this.currentLimit * 0.9
);
console.log(`๐ Decreasing limit to ${this.currentLimit}`);
}
}
// ๐งฎ Calculate system health
private calculateHealthScore(metrics: SystemMetrics): number {
const cpuScore = 100 - metrics.cpuUsage;
const memScore = 100 - metrics.memoryUsage;
const respScore = metrics.responseTime < 100 ? 100 : 10000 / metrics.responseTime;
const errorScore = 100 - metrics.errorRate;
// ๐ฏ Weighted average
return (cpuScore * 0.3 + memScore * 0.2 + respScore * 0.3 + errorScore * 0.2);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Memory Leaks in Rate Limiters
// โ Wrong way - memory leak!
class LeakyRateLimiter {
private requests: Map<string, number[]> = new Map();
checkLimit(userId: string): boolean {
const userRequests = this.requests.get(userId) || [];
userRequests.push(Date.now());
this.requests.set(userId, userRequests); // ๐ฅ Never cleaned!
return userRequests.length < 100;
}
}
// โ
Correct way - clean old data!
class CleanRateLimiter {
private requests: Map<string, number[]> = new Map();
private cleanupInterval: NodeJS.Timer;
constructor(private windowMs: number) {
// ๐งน Periodic cleanup
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, windowMs);
}
private cleanup(): void {
const cutoff = Date.now() - this.windowMs;
this.requests.forEach((timestamps, userId) => {
const valid = timestamps.filter(t => t > cutoff);
if (valid.length === 0) {
this.requests.delete(userId); // ๐๏ธ Remove empty entries
} else {
this.requests.set(userId, valid);
}
});
}
destroy(): void {
clearInterval(this.cleanupInterval); // โ ๏ธ Don't forget this!
}
}
๐คฏ Pitfall 2: Race Conditions
// โ Dangerous - race condition!
async function unsafeIncrement(redis: Redis, key: string): Promise<boolean> {
const count = await redis.get(key);
if (parseInt(count || "0") >= 100) {
return false;
}
await redis.incr(key); // ๐ฅ Another request might increment between!
return true;
}
// โ
Safe - atomic operation!
async function safeIncrement(redis: Redis, key: string): Promise<boolean> {
const result = await redis.eval(`
local current = redis.call('GET', KEYS[1]) or 0
if tonumber(current) >= tonumber(ARGV[1]) then
return 0
end
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
`, 1, key, 100, 3600); // โ
Atomic check and increment!
return result === 1;
}
๐ ๏ธ Best Practices
- ๐ฏ Choose the Right Algorithm: Fixed window for simplicity, sliding window for accuracy
- ๐ Clear Error Messages: Tell users when they can retry
- ๐ก๏ธ Defense in Depth: Combine multiple rate limiting strategies
- ๐จ Graceful Degradation: Donโt break the app, just slow it down
- โจ Monitor and Adjust: Track metrics and tune limits
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Multi-Tier Rate Limiter
Create a comprehensive rate limiting system:
๐ Requirements:
- โ Three tiers: Free (10 req/min), Basic (100 req/min), Pro (1000 req/min)
- ๐ท๏ธ Different limits for different endpoints
- ๐ค Per-user and per-IP limiting
- ๐ Daily quotas in addition to rate limits
- ๐จ Custom error responses with retry information
๐ Bonus Points:
- Add burst allowance for temporary spikes
- Implement cost-based limiting (different endpoints cost different amounts)
- Create a dashboard showing current usage
๐ก Solution
๐ Click to see solution
// ๐ฏ Our comprehensive rate limiting system!
interface UserTier {
name: "free" | "basic" | "pro";
rateLimit: number; // ๐ฆ Requests per minute
dailyQuota: number; // ๐
Daily allowance
burstAllowance: number; // ๐จ Burst capacity
}
interface EndpointCost {
path: string;
cost: number; // ๐ฐ How many "tokens" this endpoint costs
}
class MultiTierRateLimiter {
private tiers: Map<string, UserTier> = new Map([
["free", { name: "free", rateLimit: 10, dailyQuota: 1000, burstAllowance: 15 }],
["basic", { name: "basic", rateLimit: 100, dailyQuota: 10000, burstAllowance: 150 }],
["pro", { name: "pro", rateLimit: 1000, dailyQuota: 100000, burstAllowance: 1500 }]
]);
private userLimiters = new Map<string, TokenBucket>();
private ipLimiters = new Map<string, TokenBucket>();
private dailyUsage = new Map<string, number>();
constructor(private endpointCosts: EndpointCost[]) {}
// ๐ฆ Check if request is allowed
async checkRequest(
userId: string,
userTier: string,
ipAddress: string,
endpoint: string
): Promise<{
allowed: boolean;
reason?: string;
retryAfter?: number;
remainingQuota?: number;
}> {
const tier = this.tiers.get(userTier) || this.tiers.get("free")!;
const endpointCost = this.getEndpointCost(endpoint);
// ๐
Check daily quota
const dailyKey = `${userId}:${new Date().toDateString()}`;
const currentDaily = this.dailyUsage.get(dailyKey) || 0;
if (currentDaily + endpointCost > tier.dailyQuota) {
return {
allowed: false,
reason: "Daily quota exceeded ๐
",
retryAfter: this.getSecondsUntilMidnight(),
remainingQuota: tier.dailyQuota - currentDaily
};
}
// ๐ฆ Check rate limit (user)
const userBucket = this.getUserBucket(userId, tier);
if (!userBucket.consume(endpointCost)) {
return {
allowed: false,
reason: "Rate limit exceeded ๐ซ",
retryAfter: 60,
remainingQuota: tier.dailyQuota - currentDaily
};
}
// ๐ Check IP limit (prevent abuse)
const ipBucket = this.getIpBucket(ipAddress);
if (!ipBucket.consume(1)) {
// ๐ Refund user tokens
userBucket.consume(-endpointCost);
return {
allowed: false,
reason: "IP rate limit exceeded ๐",
retryAfter: 60
};
}
// โ
Update daily usage
this.dailyUsage.set(dailyKey, currentDaily + endpointCost);
return {
allowed: true,
remainingQuota: tier.dailyQuota - (currentDaily + endpointCost)
};
}
// ๐ชฃ Get or create user bucket
private getUserBucket(userId: string, tier: UserTier): TokenBucket {
const key = `user:${userId}`;
let bucket = this.userLimiters.get(key);
if (!bucket) {
bucket = new TokenBucket(
tier.burstAllowance,
tier.rateLimit / 60 // Convert to per-second
);
this.userLimiters.set(key, bucket);
}
return bucket;
}
// ๐ Get or create IP bucket
private getIpBucket(ip: string): TokenBucket {
const key = `ip:${ip}`;
let bucket = this.ipLimiters.get(key);
if (!bucket) {
// ๐ก๏ธ Stricter limits for IP to prevent abuse
bucket = new TokenBucket(50, 30 / 60);
this.ipLimiters.set(key, bucket);
}
return bucket;
}
// ๐ฐ Get endpoint cost
private getEndpointCost(endpoint: string): number {
const config = this.endpointCosts.find(e =>
endpoint.startsWith(e.path)
);
return config?.cost || 1;
}
// โฐ Calculate seconds until midnight
private getSecondsUntilMidnight(): number {
const now = new Date();
const midnight = new Date(now);
midnight.setHours(24, 0, 0, 0);
return Math.floor((midnight.getTime() - now.getTime()) / 1000);
}
// ๐ Get usage statistics
getUsageStats(userId: string): {
tier: string;
dailyUsed: number;
dailyRemaining: number;
currentRate: number;
} {
const dailyKey = `${userId}:${new Date().toDateString()}`;
const used = this.dailyUsage.get(dailyKey) || 0;
const tier = this.tiers.get("free")!; // Default for demo
return {
tier: tier.name,
dailyUsed: used,
dailyRemaining: tier.dailyQuota - used,
currentRate: 0 // Would calculate from bucket
};
}
}
// ๐ฎ Test our system!
const rateLimiter = new MultiTierRateLimiter([
{ path: "/api/simple", cost: 1 },
{ path: "/api/complex", cost: 5 },
{ path: "/api/ai", cost: 10 }
]);
// ๐งช Simulate requests
async function testRateLimit() {
const result = await rateLimiter.checkRequest(
"user123",
"basic",
"192.168.1.1",
"/api/ai"
);
if (result.allowed) {
console.log(`โ
Request allowed! Remaining quota: ${result.remainingQuota}`);
} else {
console.log(`โ ${result.reason} Retry after: ${result.retryAfter}s`);
}
}
testRateLimit();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement rate limiting with confidence ๐ช
- โ Choose the right algorithm for your use case ๐ก๏ธ
- โ Build distributed limiters for scale ๐ฏ
- โ Avoid common pitfalls like memory leaks ๐
- โ Protect your APIs from abuse! ๐
Remember: Rate limiting is your friend, not your enemy! Itโs here to help you build sustainable, scalable systems. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered API Security and Rate Limiting!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Add rate limiting to your existing APIs
- ๐ Move on to our next tutorial: Authentication Patterns
- ๐ Share your rate limiting strategies with others!
Remember: Every secure API started with proper rate limiting. Keep coding, keep learning, and most importantly, keep your APIs safe! ๐
Happy coding! ๐๐โจ