SlopBlock
Public demo
Working Demo
PR #312: Add per-user rate limiting to the upload endpoint
Adds a sliding-window rate limiter to the file upload route so each authenticated user is capped at 50 uploads per minute, returning a 429 with a `Retry-After` header when the limit is exceeded.
Diff
new file mode 100644
@@ -0,0 +1,38 @@
+import { Redis } from "ioredis";
+
+const redis = new Redis(process.env.REDIS_URL!);
+
+interface RateLimitConfig {
+ windowMs: number;
+ maxRequests: number;
+}
+
+export async function checkRateLimit(
+ userId: string,
+ config: RateLimitConfig,
+): Promise<{ allowed: boolean; retryAfterMs: number }> {
+ const key = `rate:upload:${userId}`;
+ const now = Date.now();
+ const windowStart = now - config.windowMs;
+
+ const pipeline = redis.pipeline();
+ pipeline.zremrangebyscore(key, 0, windowStart);
+ pipeline.zadd(key, now.toString(), `${now}:${Math.random()}`);
+ pipeline.zcard(key);
+ pipeline.pexpire(key, config.windowMs);
+ const results = await pipeline.exec();
+
+ const currentCount = (results?.[2]?.[1] as number) ?? 0;
+
+ if (currentCount > config.maxRequests) {
+ const oldest = await redis.zrange(key, 0, 0, "WITHSCORES");
+ const oldestTs = oldest?.[1] ? Number(oldest[1]) : now;
+ const retryAfterMs = oldestTs + config.windowMs - now;
+ return { allowed: false, retryAfterMs: Math.max(retryAfterMs, 1000) };
+ }
+
+ return { allowed: true, retryAfterMs: 0 };
+}
@@ -1,6 +1,8 @@
import { Router } from "express";
import { authenticate } from "../middleware/auth";
+import { checkRateLimit } from "../middleware/rateLimit";
import { handleUpload } from "../services/storage";
+
const router = Router();
@@ -12,6 +14,18 @@
router.post("/", authenticate, async (req, res) => {
+ const { allowed, retryAfterMs } = await checkRateLimit(req.user.id, {
+ windowMs: 60_000,
+ maxRequests: 50,
+ });
+
+ if (!allowed) {
+ const retryAfterSec = Math.ceil(retryAfterMs / 1000);
+ res.set("Retry-After", String(retryAfterSec));
+ return res.status(429).json({
+ error: "Rate limit exceeded. Try again later.",
+ });
+ }
+
const result = await handleUpload(req);
return res.status(201).json(result);
});
1
How does the rate limiter track the number of requests a user has made within the window?
src/middleware/rateLimit.ts, checkRateLimit
2
What happens if the rate limiter's Redis pipeline fails or throws an error in the upload route?
src/routes/upload.ts, src/middleware/rateLimit.ts
3
How does the endpoint tell the client when they can retry after being rate-limited?
src/routes/upload.ts, Retry-After