---
title: Rate limits
description: Add keyed request limits, understand local versus shared counters, and verify 429 responses without confusing rate limits with authorization.
pagination: false
sidebar:
  icon: gauge
---

Set a route's `rateLimit` to bound requests for a particular key during a time window. Matching HTTP middleware runs first; the route limiter then runs before input mapping and function invocation.
Rate limiting is transport policy, not authentication or a guarantee that a business operation executes once.

## Add a local policy

This source-backed compiler test demonstrates a process-local policy:

```ts title="Local rate-limit options — checked compiler example"
const route = defineRoute({
  id: "orders.local-rate-limit",
  target,
  rateLimit: { limit: 2, windowMs: 1_000, key: http.constant("global") },
});
```

In your route file, add that `rateLimit` object to the existing `defineRoute` options, keeping your
public service target and named method export. With `defineServiceRoutes`, add it to the object
containing `member`. Import `http` from `@relkit/app/routes` for the key mapping.

Here, `limit: 2` permits two requests per `windowMs: 1_000` for the constant key. It is a shared bucket
for callers of that route within one process, not a separate bucket per user. Both values must be
positive safe integers. Omitted `store` uses memory that resets with the runtime generation.

## Choose a trustworthy key

Supported key sources are a scalar `http.path`, `http.query`, `http.header`, `http.cookie`, or
`http.constant`. Body fields, catch-all arrays, transforms, optional/default wrappers, and callbacks
are not supported key mappings.

Choose a stable identity whose provenance you can verify. A client-controlled account header alone is
not a verified account: callers can rotate it to evade a limit. Enforce trusted proxy/header boundaries
or validate credentials in middleware. An absent key shares a missing-value bucket; it does not
automatically reject an unauthenticated request.

Key values are serialized into counter keys; they are **not hashed** by the limiter. Avoid putting raw
secrets in the key, logs, or metrics. Counter storage needs the same access controls as the identifiers it holds.

## Plan production counter storage

Local counters are independent per process. A production compilation rejects a rate-limited route
without an explicit `store`. The reference must resolve to a checked cache with numeric values, and
its provider must offer shared counters appropriate for the deployed replicas.

The compiler's production test checks the store reference and inferred `429` contract:

```ts title="Shared store descriptor — compiler test setup"
const store = defineCache({
  id: "api-rate-limits",
  key: z.string(),
  value: z.number(),
});
const route = defineRoute({
  id: "orders.list.http",
  target,
  rateLimit: {
    limit: 100,
    windowMs: 60_000,
    key: http.header("x-api-key"),
    store,
  },
});
```

This is compiler-level setup, not a complete domain layout or provider deployment recipe. It shows
the `store` contract; do not copy its `x-api-key` choice as advice to store secrets. Follow
[Caching](/docs/modules/caching) for cache ownership and provider configuration. Do not reach into another
domain's private cache from a route just to satisfy the reference.

An explicit store is a prerequisite, not proof of a deployment-wide quota. Verify the provider's
counter behavior under concurrency, process restarts, and multiple replicas before relying on the limit.

## Verify the response

After adding the two-per-second policy above to the [Echo endpoint](/docs/modules/http/first-route), run the
app in development and send three requests within one window:

```sh
for attempt in 1 2 3; do
  curl -i -X POST http://localhost:3000/echo \
    -H 'content-type: application/json' \
    -d '{"message":"hello"}'
done
```

Expect two `200` responses followed by `429` if all three arrive before the window resets.
The rejection body contains `error: "rate-limit"` and `retryAfterMs`. Inspect `RateLimit-Policy`,
`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `Retry-After`; reset/retry header values
use seconds. A subsequent request can succeed after the window resets.

For repeatable tests, exercise the actual server pipeline with controlled counter storage. The current
`createTestApplication` HTTP helper does not apply route rate limits. Test key isolation for nonconstant
keys and confirm rejected requests do not invoke the target. See [Testing](/docs/modules/operations/testing).

## Check which entry points are actually limited

The route limiter wraps the original HTTP method/path. It does not wrap a [raw handler](/docs/modules/http/raw-handlers)
or the generated client's `/rpc` invocation of the target. If RPC is reachable, enforce equivalent
policy at that entry point or in shared business authorization; do not assume the REST limit protects it.
See [Generated clients](/docs/modules/http/generated-clients#test-security-at-both-entry-points).

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/routes` API](/docs/api/routes)
- [`@relkit/cache` API](/docs/api/cache)

- [`tests/compiler/rate-limit.test.ts`](https://github.com/rel-kit/relkit/blob/main/tests/compiler/rate-limit.test.ts)
