Rate limits
Add keyed request limits, understand local versus shared counters, and verify 429 responses without confusing rate limits with authorization.
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:
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:
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 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, run the app in development and send three requests within one window:
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.
Check which entry points are actually limited
The route limiter wraps the original HTTP method/path. It does not wrap a raw handler
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.