rate-limiting

Protect your API with subscription-tier aware rate limiting.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "rate-limiting" with this command: npx skills add dadbodgeoff/drift/dadbodgeoff-drift-rate-limiting

Rate Limiting

Protect your API with subscription-tier aware rate limiting.

When to Use This Skill

  • Building a SaaS API with different subscription tiers

  • Protecting endpoints from abuse

  • Implementing fair usage policies

  • Adding rate limits to existing APIs

Core Concepts

Sliding Window Algorithm

More accurate than fixed windows, prevents burst at window boundaries:

Window: [----older----][----current----] Weight: 30% 70%

Tier-Based Limits

Tier Requests/min Burst

Free 60 10

Pro 600 100

Enterprise 6000 1000

TypeScript Implementation

// rate-limiter.ts import { Redis } from 'ioredis';

interface RateLimitConfig { windowMs: number; maxRequests: number; burstLimit?: number; }

interface RateLimitResult { allowed: boolean; remaining: number; resetAt: number; retryAfter?: number; }

const TIER_LIMITS: Record<string, RateLimitConfig> = { free: { windowMs: 60000, maxRequests: 60, burstLimit: 10 }, pro: { windowMs: 60000, maxRequests: 600, burstLimit: 100 }, enterprise: { windowMs: 60000, maxRequests: 6000, burstLimit: 1000 }, };

class RateLimiter { constructor(private redis: Redis) {}

async check(key: string, tier: string = 'free'): Promise<RateLimitResult> { const config = TIER_LIMITS[tier] || TIER_LIMITS.free; const now = Date.now(); const windowStart = now - config.windowMs;

const multi = this.redis.multi();

// Remove old entries
multi.zremrangebyscore(key, 0, windowStart);
// Count current window
multi.zcard(key);
// Add current request
multi.zadd(key, now.toString(), `${now}-${Math.random()}`);
// Set expiry
multi.expire(key, Math.ceil(config.windowMs / 1000) + 1);

const results = await multi.exec();
const currentCount = (results?.[1]?.[1] as number) || 0;

const allowed = currentCount &#x3C; config.maxRequests;
const remaining = Math.max(0, config.maxRequests - currentCount - 1);
const resetAt = now + config.windowMs;

return {
  allowed,
  remaining,
  resetAt,
  retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
};

} }

export { RateLimiter, RateLimitConfig, RateLimitResult, TIER_LIMITS };

Express Middleware

// rate-limit-middleware.ts import { Request, Response, NextFunction } from 'express'; import { RateLimiter } from './rate-limiter';

interface RateLimitOptions { keyGenerator?: (req: Request) => string; tierResolver?: (req: Request) => string; skip?: (req: Request) => boolean; }

function createRateLimitMiddleware( limiter: RateLimiter, options: RateLimitOptions = {} ) { const { keyGenerator = (req) => req.ip || 'unknown', tierResolver = (req) => (req as any).user?.tier || 'free', skip = () => false, } = options;

return async (req: Request, res: Response, next: NextFunction) => { if (skip(req)) return next();

const key = `ratelimit:${keyGenerator(req)}`;
const tier = tierResolver(req);
const result = await limiter.check(key, tier);

// Set rate limit headers
res.set({
  'X-RateLimit-Limit': TIER_LIMITS[tier]?.maxRequests || 60,
  'X-RateLimit-Remaining': result.remaining,
  'X-RateLimit-Reset': Math.ceil(result.resetAt / 1000),
});

if (!result.allowed) {
  res.set('Retry-After', result.retryAfter?.toString() || '60');
  return res.status(429).json({
    error: 'Too Many Requests',
    message: `Rate limit exceeded. Retry after ${result.retryAfter} seconds.`,
    retryAfter: result.retryAfter,
  });
}

next();

}; }

export { createRateLimitMiddleware };

Python Implementation

rate_limiter.py

import time from typing import Optional, NamedTuple from redis import Redis

class RateLimitResult(NamedTuple): allowed: bool remaining: int reset_at: float retry_after: Optional[int] = None

TIER_LIMITS = { "free": {"window_ms": 60000, "max_requests": 60, "burst_limit": 10}, "pro": {"window_ms": 60000, "max_requests": 600, "burst_limit": 100}, "enterprise": {"window_ms": 60000, "max_requests": 6000, "burst_limit": 1000}, }

class RateLimiter: def init(self, redis: Redis): self.redis = redis

def check(self, key: str, tier: str = "free") -> RateLimitResult:
    config = TIER_LIMITS.get(tier, TIER_LIMITS["free"])
    now = time.time() * 1000
    window_start = now - config["window_ms"]

    pipe = self.redis.pipeline()
    pipe.zremrangebyscore(key, 0, window_start)
    pipe.zcard(key)
    pipe.zadd(key, {f"{now}-{id(object())}": now})
    pipe.expire(key, int(config["window_ms"] / 1000) + 1)
    
    results = pipe.execute()
    current_count = results[1]

    allowed = current_count &#x3C; config["max_requests"]
    remaining = max(0, config["max_requests"] - current_count - 1)
    reset_at = now + config["window_ms"]

    return RateLimitResult(
        allowed=allowed,
        remaining=remaining,
        reset_at=reset_at,
        retry_after=None if allowed else int(config["window_ms"] / 1000),
    )

FastAPI Middleware

fastapi_middleware.py

from fastapi import Request, HTTPException from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware

class RateLimitMiddleware(BaseHTTPMiddleware): def init(self, app, limiter: RateLimiter): super().init(app) self.limiter = limiter

async def dispatch(self, request: Request, call_next):
    # Get user tier from request (customize based on your auth)
    tier = getattr(request.state, "user_tier", "free")
    key = f"ratelimit:{request.client.host}"
    
    result = self.limiter.check(key, tier)
    
    if not result.allowed:
        return JSONResponse(
            status_code=429,
            content={
                "error": "Too Many Requests",
                "retry_after": result.retry_after,
            },
            headers={
                "Retry-After": str(result.retry_after),
                "X-RateLimit-Remaining": "0",
            },
        )
    
    response = await call_next(request)
    response.headers["X-RateLimit-Remaining"] = str(result.remaining)
    response.headers["X-RateLimit-Reset"] = str(int(result.reset_at / 1000))
    return response

In-Memory Alternative (No Redis)

// memory-rate-limiter.ts class InMemoryRateLimiter { private windows = new Map<string, number[]>();

check(key: string, tier: string = 'free'): RateLimitResult { const config = TIER_LIMITS[tier] || TIER_LIMITS.free; const now = Date.now(); const windowStart = now - config.windowMs;

// Get or create window
let timestamps = this.windows.get(key) || [];

// Remove old entries
timestamps = timestamps.filter(t => t > windowStart);

const allowed = timestamps.length &#x3C; config.maxRequests;

if (allowed) {
  timestamps.push(now);
  this.windows.set(key, timestamps);
}

return {
  allowed,
  remaining: Math.max(0, config.maxRequests - timestamps.length),
  resetAt: now + config.windowMs,
  retryAfter: allowed ? undefined : Math.ceil(config.windowMs / 1000),
};

}

// Cleanup old entries periodically cleanup(): void { const now = Date.now(); for (const [key, timestamps] of this.windows) { const filtered = timestamps.filter(t => t > now - 60000); if (filtered.length === 0) { this.windows.delete(key); } else { this.windows.set(key, filtered); } } } }

Best Practices

  • Use Redis for distributed systems: In-memory only works for single instances

  • Set appropriate headers: Clients need X-RateLimit-* and Retry-After

  • Return 429 status: Standard HTTP status for rate limiting

  • Consider burst limits: Allow short bursts above sustained rate

  • Key by user, not just IP: Authenticated users should have their own limits

Common Mistakes

  • Using fixed windows (causes burst at boundaries)

  • Not handling Redis failures gracefully

  • Forgetting to set response headers

  • Rate limiting health check endpoints

  • Not differentiating authenticated vs anonymous users

Integration with Stripe Tiers

// Resolve tier from Stripe subscription const tierResolver = async (req: Request): Promise<string> => { const user = req.user; if (!user?.stripeSubscriptionId) return 'free';

const subscription = await stripe.subscriptions.retrieve( user.stripeSubscriptionId );

const priceId = subscription.items.data[0]?.price.id; return PRICE_TO_TIER[priceId] || 'free'; };

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

oauth-social-login

No summary provided by upstream source.

Repository SourceNeeds Review
General

sse-streaming

No summary provided by upstream source.

Repository SourceNeeds Review
General

multi-tenancy

No summary provided by upstream source.

Repository SourceNeeds Review
General

deduplication

No summary provided by upstream source.

Repository SourceNeeds Review