rate-limiting
SkillWeb & browsingImplement rate limiting on APIs. Per-IP, per-user, sliding window patterns. Express and framework-agnostic.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the rate-limiting skill
What this skill tells your AI
The instructions your AI receives, as published by porcupine-md/anoa-browser in .claude/skills/library/security/rate-limiting/SKILL.md and read by ahel’s review.
Rate Limiting Strategies
Per-IP (unauthenticated endpoints)
Protects login, registration, password-reset from brute force.
Per-User (authenticated endpoints)
Fair use — prevents a single user from hammering your API.
Per-Route (sensitive endpoints)
Extra tight limits on auth, payment, export endpoints.
Express Implementation (express-rate-limit)
import rateLimit from 'express-rate-limit';
import { Redis } from 'ioredis';
import { RedisStore } from 'rate-limit-redis';
const redis = new Redis(process.env.REDIS_URL);
// General API rate limit
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true, // Return rate limit info in RateLimit-* headers
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
handler: (req, res) => {
res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please try again later.',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
},
});
},
});
// Strict limit for auth endpoints
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 login attempts per 15 minutes
skipSuccessfulRequests: true, // don't count successful logins
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
keyGenerator: (req) => `auth:${req.ip}:${req.body?.email || ''}`,
});
// Usage
app.use('/api', apiLimiter);
app.post('/api/auth/login', authLimiter, loginHandler);
Per-User Limit (After Authentication)
export const perUserLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // 60 requests per minute per user
keyGenerator: (req) => `user:${req.user?.id || req.ip}`,
});
app.use('/api', authenticate, perUserLimiter);
Sliding Window with Redis (Manual)
async function isRateLimited(key: string, limit: number, windowSeconds: number): Promise<boolean> {
const now = Date.now();
const windowStart = now - windowSeconds * 1000;
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart); // remove old entries
pipeline.zadd(key, now, `${now}`); // add current request
pipeline.zcard(key); // count in window
pipeline.expire(key, windowSeconds); // auto-cleanup
const results = await pipeline.exec();
const count = results![2][1] as number;
return count > limit;
}
Response Headers (Standard)
RateLimit-Limit: 100
RateLimit-Remaining: 75
RateLimit-Reset: 1704067200
Retry-After: 300 (only on 429)
Testing Rate Limits
it('blocks after limit exceeded', async () => {
for (let i = 0; i < 5; i++) {
await request(app).post('/api/auth/login').send(validCreds);
}
const response = await request(app).post('/api/auth/login').send(validCreds);
expect(response.status).toBe(429);
expect(response.body.error.code).toBe('RATE_LIMIT_EXCEEDED');
});
Signals
- GitHub stars
- 23
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
rate-limiting- Source
- github.com/porcupine-md/anoa-browser