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
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
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'),
});