The tempting wrong place
Most web frameworks make it easy to attach a rate limit to a single route, right next to the handler. It reads well and it feels tidy — the limit lives with the thing it protects. It is also the wrong place, and for a reason that only shows up under load: by the time a per-route limit runs, the request has already crossed the network, occupied a worker, and been parsed. The limiter rejects it, but the service already paid for it. Under a flood, the app spends all its capacity saying no.
Scattering limits across routes has a second cost. Now the policy lives in a dozen handlers, and there is no single place to read what your limits actually are or to change them together. The rule fragments, the numbers drift, and the source of truth becomes whatever the code happens to say in each spot — which is how limits end up inconsistent in exactly the moment you need them coherent.
At the edge, keyed by IP
The right place is the edge — the reverse proxy in front of the application, keyed by client address. There, an abusive source is turned away before it reaches a worker, before a single application cycle is spent, and the policy lives in one file that describes the whole surface at once. The app stays free to do its actual job, because the flood never arrives at its door. One place to read the rule, one place to change it, and rejection that costs almost nothing.
This is a global default across everything we run, and it is the shape we bring to the services and interfaces we build for clients. An edge limit is defense you can reason about because it is centralised; a per-route limit is defense that has already let the attacker in far enough to make you pay. The difference is where the no gets said.
The one honest exception
There is exactly one case where a limit belongs in the app, and it is not really the same kind of limit. Some limits are per user or per customer and depend on identity or account state — a cap on how much a single tenant can spend on model calls, a quota on a customer's use of an expensive feature. Enforcing those needs the app to inspect a token or read the database, which the edge cannot do, because the edge only knows the address, not the account.
So the split is clean. Traffic-shaped limits — is this address hammering us — live at the edge. Business-shaped limits — has this customer used their allowance — live in the app, because only the app knows who the customer is. Keeping the two apart is what stops the edge from needing to understand billing and the app from having to fend off floods it should never have seen. It is one thread of the same secure-by-default posture we build in from the start.