Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

Reference and source