2026年11月13日

2026年11月13日

Limit Login Attempts in WordPress: Plugin, Code, Nginx and Apache

This article is a localized edition of the Japanese original. Read the Japanese original

WordPress core has no mechanism that counts failed logins and locks the visitor out for a while. Out of the box, an attacker can keep guessing passwords (a brute-force attack) as many times as they like, so it is worth adding a limit on login attempts.

This article covers the four methods below. Everything was checked as of November 2026.

Method Best for
Plugin (Limit Login Attempts Reloaded) You want to configure everything in the admin screen. Enough for most sites
Your own mu-plugin You don't want another plugin and prefer a small piece of code you understand
Nginx limit_req You can edit the server config and want to throttle requests before they reach PHP
Apache IP restriction on the login screen Your administrators connect from known IP addresses

To protect accounts even if a password leaks, also consider two-factor authentication (How to set up two-factor authentication in WordPress).

Method 1: Use Limit Login Attempts Reloaded

This is a free plugin from wordpress.org. Its listing is now titled "Limit Login Attempts Security", but the plugin slug is still limit-login-attempts-reloaded (the latest version as of November 2026 is 3.4.2).

Installation

In the admin screen, go to Plugins → Add New Plugin, search for "Limit Login Attempts", install it and activate it. If you have WP-CLI, run:

wp plugin install limit-login-attempts-reloaded --activate

Settings screen and defaults

Once activated, a "Limit Login Attempts" item appears in the left admin menu (if you turn off the left menu item in its settings, it moves under Settings). In the "Local App" section of the "Settings" tab you can change the values below. The defaults were checked in the plugin's source code.

Field Meaning Default
allowed retries Failed attempts allowed before a lockout 4
minutes lockout Length of one lockout 20 minutes
lockouts increase lockout time to … hours After this many lockouts, switch to a long lockout 24 hours after 4 lockouts
hours until retries are reset Time before the failure count is reset 24 hours

On the same screen, "Notify on lockout" lets you email the administrator when a lockout happens.

Keeping your own IP from being locked out

Add your IP address to the "Safelist" on the "Logs" tab and that IP will never be locked out. IPs on the "Denylist" cannot log in at all.

If a CDN such as Cloudflare or a reverse proxy sits in front of your site, the client address PHP sees (REMOTE_ADDR) is the proxy's IP, and every visitor is counted as the same IP. In "Trusted IP Origins" on the "Settings" tab, specify the server variable that holds the real client address (for example HTTP_CF_CONNECTING_IP or HTTP_X_FORWARDED_FOR). On a site without a proxy, setting these lets attackers forge the header and slip past the limit, so match the setting to your actual setup.

Removing a lockout

If another administrator can log in, the "Active lockouts" list on the "Logs" tab shows the blocked IPs, and "Unlock" removes a lockout.

If you locked yourself out, the plugin's FAQ suggests the following.

  1. Log in from a different IP, such as your phone's mobile connection or a VPN
  2. If that is not possible, use FTP or your host's file manager to temporarily rename the wp-content/plugins/limit-login-attempts-reloaded folder, which disables the plugin. Log in, rename the folder back, and add your IP to the Safelist

On a single site with WP-CLI, the list of locked-out IPs is stored in the limit_login_lockouts option (pairs of IP address and release time). Emptying it as shown below removes every lockout.

# Check the current lockouts
wp option get limit_login_lockouts --format=json

# Remove all lockouts
wp option update limit_login_lockouts '[]' --format=json

This storage format is an internal detail of the plugin, so normally use the admin screen instead.

Method 2: Write your own mu-plugin

This example adds a minimal limit without installing a plugin. The wp_login_failed action counts failures, and the authenticate filter decides whether to block.

On the authenticate filter, WordPress core checks the username and password at priority 20. Checking at priority 30 means a locked-out visitor cannot log in even with the correct password. If you counted failures inside authenticate instead, a correct password could still get through during a lockout, so counting is done separately in wp_login_failed.

Save it as wp-content/mu-plugins/login-limit.php (create the mu-plugins folder if it does not exist; files in it load automatically and cannot be deactivated from the admin screen).

<?php
/**
 * Plugin Name: Login Limit (simple)
 * Description: Block logins for a while after repeated failures from the same IP.
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

const MY_LOGIN_MAX_FAILURES = 5;    // Failures before a lockout
const MY_LOGIN_LOCK_SECONDS = 1200; // Lockout length (seconds)

function my_login_limit_key() {
	$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
	return 'my_login_fail_' . md5( $ip );
}

// Count each failed login.
add_action( 'wp_login_failed', function () {
	$key   = my_login_limit_key();
	$count = (int) get_transient( $key );
	set_transient( $key, $count + 1, MY_LOGIN_LOCK_SECONDS );
} );

// Once the limit is reached, block the login even if the password is correct.
add_filter( 'authenticate', function ( $user, $username ) {
	if ( '' === trim( (string) $username ) ) {
		return $user;
	}
	if ( (int) get_transient( my_login_limit_key() ) >= MY_LOGIN_MAX_FAILURES ) {
		return new WP_Error(
			'too_many_retries',
			'Too many failed login attempts. Please wait a while and try again.'
		);
	}
	return $user;
}, 30, 2 );

How it behaves, and what to watch for:

  • A login attempt during a lockout also counts as a failure and resets the remaining lockout time to 20 minutes.
  • The count is stored in a transient (the _transient_my_login_fail_… option, or the object cache). To lift a lockout, run wp transient delete my_login_fail_<md5> with the md5 of the IP, or clear them all with wp transient delete --all (this also deletes other transients, which are normally regenerated).
  • Behind a CDN or reverse proxy, REMOTE_ADDR is the proxy's IP. In that case, have the server replace it with the real client address first (Nginx's real_ip module or Apache's mod_remoteip). Trusting forwarded headers directly in PHP lets attackers bypass the limit by forging them.
  • Logins through XML-RPC (xmlrpc.php) also pass through authenticate, so they are limited too. If you don't use XML-RPC, turning it off also helps (How to disable XML-RPC in WordPress).

Method 3: Throttle with Nginx limit_req

If you can configure Nginx yourself, you can rate-limit only POST requests to wp-login.php (login submissions) per IP. Blocked requests never reach PHP, which also keeps server load down.

Add the following to the http block (/etc/nginx/nginx.conf or a file in conf.d/*.conf). The map leaves the key empty for GET requests. limit_req_zone does not count requests with an empty key, so simply viewing the login screen is not limited.

map $request_method $wp_login_limit_key {
    POST    $binary_remote_addr;
    default "";
}

# Up to 6 per minute per IP (one every 10 seconds on average)
limit_req_zone $wp_login_limit_key zone=wp_login:10m rate=6r/m;

Add a location for wp-login.php to your site's server block. For the part that passes requests to PHP, copy what is already in your location ~ \.php$ block (the include and fastcgi_pass lines below are an example for PHP 8.3 on Debian / Ubuntu).

location = /wp-login.php {
    limit_req zone=wp_login burst=5 nodelay;
    limit_req_status 429;

    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
  • burst=5 nodelay accepts up to 5 requests in a short burst without delay and returns 429 for anything beyond that.
  • location = /wp-login.php is an exact match, so it takes priority over location ~ \.php$.
  • After editing, check the syntax with sudo nginx -t and then apply it with sudo systemctl reload nginx.

This only slows requests down; it does not lock anyone out after a number of failures. Use it together with Method 1 or Method 2.

Method 4: Restrict the login screen by IP in Apache

If your administrators connect from fixed IP addresses, allowing wp-login.php only from those IPs stops most attacks at the door, rather than counting attempts. In Apache 2.4, write the following at the top of the .htaccess file in your WordPress directory.

<Files "wp-login.php">
    Require ip 203.0.113.10
    Require ip 198.51.100.0/24
</Files>
  • With several Require ip lines, a match on any one of them is allowed. All other requests get a 403.
  • The Apache 2.2 directives Order deny,allow / Deny from all / Allow from are legacy syntax in 2.4 (they only work with mod_access_compat).
  • To use Require in .htaccess, the server must allow AllowOverride AuthConfig (or All).
  • This does not work for sites where people other than administrators log in, such as membership sites. If your own IP changes, you will lock yourself out too.

For more detail, see How to restrict the WordPress login page by IP.

Things to keep in mind

  • Where many people share one IP, such as an office or school network, one person's failures can lock everyone out. Don't set the allowed failures too low.
  • A limit only slows down brute-force attacks. Combine it with hard-to-guess passwords and two-factor authentication.
  • Running the plugin and your own code together double-counts failures and locks people out sooner than expected. Pick one.

Summary

  • The quickest start: install Limit Login Attempts Reloaded, add your IP to the Safelist, and set Trusted IP Origins if you use a CDN.
  • If you'd rather not add a plugin, an mu-plugin that counts in wp_login_failed and blocks in authenticate (priority 30) does the same job.
  • If you can edit the server config, Nginx limit_req or Apache Require ip filters requests before they reach PHP, which makes the protection more effective.

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲