The hook reads contract.metadata.rateLimit. Global and IP-scoped limits run
in onRequest before context creation; user-scoped limits run in
beforeHandle after route hooks have resolved identity and ctx.actor is
available. Exceeded limits throw the framework TooManyRequests app error
with scope, retryAfterSeconds, and resetAt details, and the 429
response carries a Retry-After header when the limiter reports a reset
time. The bucket key is
never sent to clients; denials emit a rateLimit.denied instrumentation
event that carries the key for operators.
ip-scoped limits require an explicit trustedProxy.clientIp, ipSource,
or custom earlyKey: the hook's validate phase fails createServer(...)
startup when a registered contract declares an ip scope without one,
instead of silently collapsing all clients into a shared ip:unknown bucket.
Contracts added later through server.route(...) are not visible to
validate, so enforcing an ip-scoped limit without a client-IP source
throws the same configuration error at request time as a backstop. Pass
ipSource: "none" to explicitly opt in to the shared ip:unknown bucket.
Create metadata-driven rate-limit hooks.
The hook reads
contract.metadata.rateLimit. Global and IP-scoped limits run inonRequestbefore context creation; user-scoped limits run inbeforeHandleafter route hooks have resolved identity andctx.actoris available. Exceeded limits throw the frameworkTooManyRequestsapp error withscope,retryAfterSeconds, andresetAtdetails, and the 429 response carries aRetry-Afterheader when the limiter reports a reset time. The bucket key is never sent to clients; denials emit arateLimit.deniedinstrumentation event that carries the key for operators.ip-scoped limits require an explicittrustedProxy.clientIp,ipSource, or customearlyKey: the hook'svalidatephase failscreateServer(...)startup when a registered contract declares anipscope without one, instead of silently collapsing all clients into a sharedip:unknownbucket. Contracts added later throughserver.route(...)are not visible tovalidate, so enforcing anip-scoped limit without a client-IP source throws the same configuration error at request time as a backstop. PassipSource: "none"to explicitly opt in to the sharedip:unknownbucket.