rate-limiting

SkillWeb & browsing

Implement 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.

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