A login page with no limit can be tried as fast as the network allows. A limit does not make a weak password strong; it makes guessing take longer than anyone will wait, which in practice is the same thing.

At the web server

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

location = /wp-login.php {
    limit_req zone=login burst=3 nodelay;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Five a minute with a burst of three: nobody types faster, and a machine is stopped dead. Do this at the server, not in the application - a request refused here never reaches PHP at all.

Watch it before you enforce it

grep -c "limiting requests" /var/log/nginx/error.log
A limit set too low locks out real people, and they do not report it - they leave. Watch the log for a week at a generous rate before tightening.

Ban repeat offenders

# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
bantime = 3600

What a limit does not do

  • It does not stop a distributed attempt from a thousand addresses, each trying twice.
  • It does not help if the password already leaked - the attacker needs one attempt.
Two-factor makes rate limiting almost unnecessary and rate limiting does not make two-factor unnecessary. If you only do one, do two-factor.