Rate limiting in nginx before it reaches PHP

A rate limiter in the application still costs a PHP-FPM worker per request, which means a burst of login attempts occupies the pool whether or not it is rejected. nginx can refuse them for the price of a hash lookup.

limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

location = /login {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;
}

burst without nodelay queues the excess and releases it at the configured rate, which turns a spike into a slow trickle; with nodelay the burst is allowed immediately and the rest rejected. Ten megabytes of zone holds roughly 160,000 addresses. Behind a proxy or CDN, $binary_remote_addr is the proxy — set_real_ip_from has to be configured first or the whole world shares one bucket.