Rate limiting at the web server is cheaper than in the application and protects the endpoints the application never gets to serve.
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location = /wp-login.php {
limit_req zone=login burst=3 nodelay;
limit_req_status 429;
}
# 10m holds about 160,000 addresses. $binary_remote_addr
# rather than $remote_addr because it is 4 bytes, not 15.
burst without nodelay queues the excess requests and serves them slowly, which is usually worse than refusing — a queued request holds a worker connection. With nodelay the burst is served immediately and the budget is still consumed, which is what most people mean. Behind a proxy, $binary_remote_addr is the proxy, so the zone key has to be the forwarded address and the proxy has to be trusted for that to mean anything.