Limit how often a route is called
You end with a public form limited to five requests an hour, an API's own limit, and clients counted properly behind a proxy.
On this page
Every route already has a rate limit, so you write nothing to get one: 600 requests a minute for each signed-in caller, and 120 a minute for each address on routes open to anyone. You change a limit when a route needs a different one, such as a public form that should take far fewer. This guide limits a "request a demo" form to five requests an hour from each address.
You start from an app with a DemoRequest entity and an action,
request_demo, that keeps one.
Store and change a record shows how to write
those.
Limit one route
rate_limit: goes on the route. requests: is how many it takes and within:
is the period, in whole seconds from 1s to 24h.
domains/:
use sales/types { DemoForm, DemoReceived }use sales/actions { request_demo }/// Requests for a demo.routes DemoRequests { prefix: /demo-requests /// Asks for a demo. POST / { authentication: RouteAuthentication.none rate_limit: { requests: 5 within: 1h by: RateLimitKey.ip } input: DemoForm output: DemoReceived status: SuccessStatus.created run: request_demo(input) }}
by: says what is counted:
RateLimitKey.ipcounts each address.RateLimitKey.caller, which you get if you leaveby:out, counts each signed-in caller, or each address when the caller isn't signed in.
A limit you write replaces the default for that route, so it can loosen a limit as well as tighten it. This route's requests have a count of their own and use none of the caller's default allowance.
jadpo routes shows each route's limit, and the route's OpenAPI entry lists it
as x-jadpo-rate-limit, so a client can see it:
jadpo routesmain / POST /demo-requests action request_demo none 5/1h ip domains/sales/routes/demo_requests.jadpo:9 GET /openapi.json generated openapi none generated2 routes: 1 API
Try it
A route test never gets a 429, because a test's host counts nothing. You see a
limit on jadpo dev and in a deployed app, so start jadpo dev, and send the
form five times in another terminal:
for i in 1 2 3 4 5; do curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:3000/demo-requests \ -H 'content-type: application/json' -d '{"company": "Acme"}'done
201201201201201
The sixth is refused:
curl -i -X POST http://localhost:3000/demo-requests \ -H 'content-type: application/json' -d '{"company": "Acme"}'
HTTP/1.1 429 Too Many Requestscontent-type: application/jsonretry-after: 3600ratelimit-policy: "default";q=5;w=3600ratelimit: "default";r=0;t=3600x-request-id: req_70033ff2b9581e87content-length: 100date: Wed, 04 Nov 2026 11:18:18 GMT{"error":{"code":"rate_limited","message":"Too many requests.","request_id":"req_70033ff2b9581e87"}}
Retry-After is the seconds to wait. RateLimit-Policy holds the limit's
requests (q) and its period in seconds (w), and RateLimit says none are
left (r=0) for t seconds. A refused request never reaches your action, so
you write no code for it. rate_limited is a failure every route can give, and
its message comes from translations like any other failure's.
Change the limit for a whole API
rate_limit: on an API replaces the default limit for its routes that write
none of their own, and they share one count, as they share the default. Routes
Jadpo answers itself, such as sign-in and webhooks, keep their own limits.
app.jadpo:
application { apis: { main: { prefix: / rate_limit: { requests: 300 within: 1m } } } client_address: { source: ClientAddressSource.forwarded_for hops: config.proxy_hops }}
Count clients behind a proxy
The client_address: above matters as soon as your app runs behind a load
balancer. A native app takes an address from its connection, which would be the
balancer's, so all your clients would share one allowance. client_address:
says which header holds the real address, and how many proxies of your own
to believe, so a caller can't choose an address by sending that header itself.
On Workers, you write nothing: Jadpo reads Cloudflare's header.
config.jadpo:
use sales/types { ProxyHops }config { proxy_hops: ProxyHops { default: 1 }}
domains/:
/// How many proxies of our own stand in front of the app.type ProxyHops = Int { min: 1 max: 5}
Know what a limit doesn't do
A limit counts each caller or each address, never everyone together, so a client with many addresses gets the allowance many times. It makes a public route more costly to abuse, but it isn't a budget. A route that spends money on each call needs a cap of its own as well.
Where the counts are kept decides how exact they are:
- A native app counts in each server's memory. Several servers each give the whole allowance, and a restart starts every count again.
- On Cloudflare Workers a Worker's instances share no memory, so a deployable must name a store, Cloudflare's rate-limiting binding. Deploy to Cloudflare Workers sets it up. Its counts are per Cloudflare location, and rough.
Read more
Rate limits, Rate-limited requests, Client addresses and Rate limits on Workers.
Not built yet
- Sharing counts between native servers through Redis isn't built yet. A deployable whose store is Redis counts in its own memory for now.
- Turning a limit off has no syntax yet. To loosen one, raise
requests:or shortenwithin:. - A generated route's own limits aren't shown yet in
jadpo routesor OpenAPI.
Markdown for agents: /guides/guide-limit-how-often-a-route-is-called.md.Built from Jadpo main at 94ab778. Jadpo's CI compiles this guide's code and runs its tests.