Limit Login Attempts Without a Plugin
Locks out an IP address after repeated failed login attempts using WordPress transients, giving you basic brute-force protection with no dedicated plugin.
- Difficulty
- Intermediate
- Reading time
- ~2 min
- Where to add it
- A snippet manager plugin (e.g. WPCode, Code Snippets)
- Last updated
The Problem
WordPress core has no built-in rate limiting on wp-login.php — a script can submit thousands of username and password combinations against it with no delay and no lockout, limited only by your server's raw request-handling capacity. This is the classic WordPress brute-force pattern: attackers aren't trying to break encryption, they're just trying common passwords against common usernames (admin, the site's own name, etc.) at high volume, betting that some fraction of sites will have a weak enough combination to hit. A dedicated security plugin can solve this, but if you're already running a lean plugin stack, a small snippet that tracks failed attempts per IP and blocks further tries after a threshold covers the same core risk without adding another plugin to maintain.
The Code
add_action( 'wp_login_failed', 'devmubs_track_failed_login' );
function devmubs_track_failed_login( $username ) {
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'devmubs_login_attempts_' . md5( $ip );
$count = (int) get_transient( $key );
set_transient( $key, $count + 1, 15 * MINUTE_IN_SECONDS );
}
add_filter( 'authenticate', 'devmubs_block_locked_out_ip', 30, 1 );
function devmubs_block_locked_out_ip( $user ) {
$ip = $_SERVER['REMOTE_ADDR'];
$key = 'devmubs_login_attempts_' . md5( $ip );
$count = (int) get_transient( $key );
if ( $count >= 5 ) {
return new WP_Error(
'too_many_attempts',
__( 'Too many failed login attempts. Please try again in 15 minutes.' )
);
}
return $user;
}How It Works
The `wp_login_failed` action fires every time a login attempt fails for any reason, and doesn't receive the requesting IP directly — so the callback reads it from `$_SERVER['REMOTE_ADDR']` and uses it to build a per-IP transient key, incrementing a counter that auto-expires after 15 minutes (so the lockout window resets itself with no cleanup job needed). The `authenticate` filter runs earlier in the login process, before WordPress even finishes checking the submitted password, which is exactly why it's used for the block: once an IP's counter reaches 5 within that 15-minute window, this callback returns a `WP_Error` instead of the user object, which WordPress treats as an authentication failure and stops the login attempt from proceeding any further — the real password never even gets checked once an IP is locked out. Priority `30` on the filter ensures this runs after WordPress's own core authentication checks have already run, so it's blocking on top of core behavior, not instead of it.
Where To Add This
A snippet manager plugin (e.g. WPCode, Code Snippets). This has no dependency on your theme and is simple enough to toggle on or off instantly if it ever misbehaves, which is exactly what a snippet manager is for — you get an on/off switch without touching a theme file under pressure.
Warnings & Caveats
- If your site sits behind Cloudflare or another reverse proxy, `$_SERVER['REMOTE_ADDR']` may return the proxy's IP for every visitor, which would lock out all traffic at once instead of a single attacker — use a validated `X-Forwarded-For` header in that setup instead.
- This only limits standard wp-login.php attempts. Pair it with the Disable XML-RPC snippet, since XML-RPC's system.multicall method is a separate brute-force path this doesn't cover.
- Always back up your site before adding custom code.
- Always back up your site before adding custom code.