The WordPress login page at /wp-login.php is one of the most aggressively targeted URLs on the internet. Automated bots continuously probe it with credential lists, and the default WordPress installation offers no rate limiting, no lockout mechanism, and no obscurity — the URL is always the same, and failed logins generate no friction. The good news is that three inexpensive measures together reduce brute-force risk dramatically: limiting login attempts with a lockout (blocks bots after a handful of tries), disabling XML-RPC (closes a second authentication surface that most sites do not use), and optionally renaming the login URL (removes the site from automated scans that target the default path). None of these require a security mega-plugin — each can be implemented in fewer than fifteen lines of code and one .htaccess rule. This article implements all three without external dependencies.
Problem: Your WordPress login page is open to brute-force attempts with no rate limiting, and XML-RPC provides a second authentication surface that attackers can hammer with no lockout.
Solution: Implement a login attempt limiter using transients, disable XML-RPC via a filter and .htaccess, and optionally block direct access to wp-login.php for everyone except whitelisted IPs.
Login attempt limiter — stores failure counts in transients and blocks the IP after 5 failed attempts for 15 minutes:
<?php
add_filter( 'authenticate', 'check_attempted_login', 30, 3 );
function check_attempted_login( $user, $username, $password ) {
if ( empty( $username ) && empty( $password ) ) {
return $user;
}
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ?? '' );
$key = 'failed_login_' . md5( $ip );
$attempts = (int) get_transient( $key );
if ( $attempts >= 5 ) {
return new WP_Error(
'too_many_retries',
__( 'Too many failed login attempts. Please try again in 15 minutes.' )
);
}
return $user;
}
add_action( 'wp_login_failed', 'on_login_failed' );
function on_login_failed( $username ) {
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ?? '' );
$key = 'failed_login_' . md5( $ip );
set_transient( $key, (int) get_transient( $key ) + 1, 15 * MINUTE_IN_SECONDS );
}
add_action( 'wp_login', 'on_login_success', 10, 2 );
function on_login_success( $user_login, $user ) {
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ?? '' );
delete_transient( 'failed_login_' . md5( $ip ) );
}
Disable XML-RPC entirely (add to functions.php):
<?php
add_filter( 'xmlrpc_enabled', '__return_false' );
Block xmlrpc.php and restrict wp-login.php to a specific IP at the server level — add to .htaccess before the WordPress rewrite block:
# Block XML-RPC entirely
<Files xmlrpc.php>
Order deny,allow
Deny from all
</Files>
# Restrict wp-login.php to your office IP (replace with your real IP)
<Files wp-login.php>
Order deny,allow
Deny from all
Allow from 203.0.113.10
</Files>
NOTE: The IP whitelist in .htaccess will lock you out if your IP changes (common with home or mobile internet connections). If you use a dynamic IP, skip the wp-login.php block and rely on the transient-based limiter instead. For production sites on managed hosting where .htaccess is not available, the same result can be achieved with Nginx limit_req directives or a WAF rule.