Security

Rate Limiting

Protecting your API routes from abuse with Cloudflare KV.

Modern applications need rate limiting to protect against abuse, manage resources efficiently, and enable tiered API access. Without rate limiting, your application is vulnerable to brute force attacks, scraping, and potential service disruptions from excessive usage.

@repo/rate-limit is a fixed-window limiter backed by a Cloudflare Workers KV binding (RATE_LIMIT_KV). There is no Redis URL or token — the Wrangler binding is the contract.

The REST interface is designed to rate limit per AgentToken, returning 429 with Retry-After and the remaining budget. That is not built; nothing below applies to it yet.

Setting up

Bind a dedicated KV namespace on the Worker that needs limits. apps/web already declares RATE_LIMIT_KV in wrangler.jsonc (separate from NEXT_INC_CACHE_KV).

The marketing contact form limits submissions to 1 per day per IP when that binding is present (OpenNext preview / deploy). Plain next dev without Cloudflare context skips the limiter so local marketing still works. If KV I/O fails in a Worker, the limiter fails closed and the request is denied.

Adding rate limiting

apps/app/api/chat/route.ts
import { createRateLimiter, slidingWindow } from '@repo/rate-limit';

const rateLimiter = createRateLimiter({
  limiter: slidingWindow(10, '10 s'),
});

export const POST = async (request: Request) => {
  const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
  const { success } = await rateLimiter.limit(`chat_${ip}`);

  if (!success) {
    return new Response('Too many requests', { status: 429 });
  }

  // Handle the request
};

slidingWindow(max, window) accepts strings like "10 s", "5 m", "2h", and "1d". The implementation is a fixed window stored in KV (count + windowStart, with expirationTtl ≥ 60). Concurrent Workers may slightly over-admit because KV is eventually consistent — acceptable for contact-form spam control.

Contact form

apps/web/app/[locale]/(marketing)/contact/actions/contact.tsx
const rateLimiter = createRateLimiter({
  limiter: slidingWindow(1, '1d'),
});
const { success } = await rateLimiter.limit(`contact_form_${ip}`);

Testing

Inject an in-memory KV in unit tests:

import { createMemoryKv, createRateLimiter, slidingWindow } from '@repo/rate-limit';

const limiter = createRateLimiter({
  kv: createMemoryKv(),
  limiter: slidingWindow(2, '10 s'),
});

On this page

GitHubEdit this page on GitHub