Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/middleware/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import redis from '../config/redis.js';

// Configuration: Allow 100 requests per 60 seconds per client
const WINDOW_SIZE_IN_SECONDS = 60;
const MAX_REQUESTS_ALLOWED = 100;

export const rateLimiter = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
// Identify client by authenticated client ID, falling back to IP address
const clientId = req.client?.id || req.ip || 'anonymous';
const redisKey = `rate_limit:${clientId}`;

try {
// TODO: [Exercise - Redis Sliding Window Rate Limiter]
// Implement a sliding-window rate limiter using Redis sorted sets (zset).
//
// Algorithm Steps:
// 1. Get the current timestamp in milliseconds (e.g., const now = Date.now()).
// 2. Define the window boundary (e.g., const clearBefore = now - WINDOW_SIZE_IN_SECONDS * 1000).
// 3. Execute a Redis transaction (pipeline) to perform these operations atomically:
// a. ZREMRANGEBYSCORE: Remove elements (timestamps) older than 'clearBefore'.
// b. ZADD: Add the current timestamp 'now' with a unique member value (e.g. timestamp or random string) to the sorted set.
// c. ZCARD: Get the count of active members in the sorted set.
// d. EXPIRE: Update the key's TTL to at least WINDOW_SIZE_IN_SECONDS to clean up idle keys.
// 4. Retrieve the count from the transaction results.
// 5. If the count exceeds MAX_REQUESTS_ALLOWED:
// - Set HTTP headers: 'Retry-After' (in seconds) and 'X-RateLimit-Limit' / 'X-RateLimit-Remaining'.
// - Return an HTTP 429 Too Many Requests response with a JSON error body.
// 6. If the count is within limits, call next() to allow the request.
//
// Resources:
// - ioredis transactions: redis.multi().zremrangebyscore(...).zadd(...).zcard(...).pexpire(...).exec()

// Bypassing rate limiting for now (skeleton fallback)
next();
} catch (error) {
console.error('❌ Rate Limiter Error:', error);
// In production, we fail-open (call next()) to prevent a Redis crash from taking down the API,
// or fail-closed based on risk posture. Let's fail-open for now.
next();
}
};
2 changes: 2 additions & 0 deletions src/routes/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { validate } from '../middleware/validate.js';
import { CreateEventSchema } from '../schemas/event.js';
import { authenticate } from '../middleware/auth.js';
import { enforceIdempotency } from '../middleware/idempotency.js';
import { rateLimiter } from '../middleware/rateLimiter.js';
import redis from '../config/redis.js';
import { producer } from '../config/kafka.js';

Expand All @@ -13,6 +14,7 @@ const KAFKA_TOPIC = 'metrics.raw';
router.post(
'/events',
authenticate,
rateLimiter,
enforceIdempotency,
validate(CreateEventSchema),
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
Expand Down