Skip to content

Rate Limiting ​

First-class rate limiting backed by Redis/Valkey. Use as global middleware or per-route.

Per-route rate limiting ​

ts
import { api } from "cooper-stack/api";
import { rateLimit } from "cooper-stack/rateLimit";

export const login = api(
  {
    method: "POST",
    path: "/auth/login",
    middleware: [rateLimit({ window: "1m", max: 5 })],
  },
  async (input) => {
    return await authenticate(input.email, input.password);
  }
);

This limits /auth/login to 5 requests per minute per IP address.

Global rate limiting ​

Apply to all routes:

ts
import { cooper } from "cooper-stack/middleware";
import { rateLimit } from "cooper-stack/rateLimit";

cooper.use(rateLimit({ window: "1m", max: 100 }));

Custom key function ​

By default, rate limiting is keyed by IP address. Use key to customize:

ts
// Rate limit by API key
rateLimit({
  window: "1m",
  max: 100,
  key: (req) => req.headers["x-api-key"] ?? "anonymous",
});

// Rate limit by authenticated user
rateLimit({
  window: "1h",
  max: 1000,
  key: (req) => req.headers["authorization"] ?? req.ip,
});

Response ​

When the limit is exceeded, Cooper returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 45
Content-Type: application/json

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Try again in 45s.",
    "retryAfter": 45
  }
}

The Retry-After header tells clients exactly how long to wait.

Sliding window algorithm ​

The rate limiter uses a sliding window based on Redis INCR with TTL:

  1. Key format: cooper:rl:{identifier}:{window_bucket}
  2. Each request increments the counter atomically
  3. TTL is set on first increment (expires with the window)
  4. When count exceeds max, request is rejected

This approach is simple, atomic, and accurate to the window granularity.

Combining rate limits ​

Stack multiple rate limiters for tiered protection:

ts
export const createPost = api(
  {
    method: "POST",
    path: "/posts",
    middleware: [
      rateLimit({ window: "1s", max: 2 }),    // burst protection
      rateLimit({ window: "1h", max: 100 }),   // hourly cap
    ],
  },
  async (input) => {
    return await createPost(input);
  }
);

Config reference ​

OptionTypeDefaultDescription
windowstringrequiredTime window ("10s", "1m", "1h", "1d")
maxnumberrequiredMax requests within the window
key(req) => stringIP addressFunction to derive the rate limit key

Apache-2.0 Licensed