2026年11月12日
2026年11月12日
Email Alerts for Failed WordPress Logins with the wp_login_failed Hook
This article is a localized edition of the Japanese original. Read the Japanese original
The WordPress login screen (wp-login.php) often starts receiving brute-force login attempts as soon as a site goes live. If you notice early that attempts are piling up, you can act: review passwords, restrict access, and so on.
In this article we use WordPress's wp_login_failed hook to build a mechanism that emails the administrator when failed logins from the same IP address reach a set number. We also cover notifications when an administrator logs in, and how to do it with a plugin instead. The code was tested on WordPress 6.9 as of October 2026.
How it works
When a login fails, WordPress fires the wp_login_failed action.
do_action( 'wp_login_failed', $username, $error );
| Argument | Contents |
|---|---|
$username |
The username or email address that was entered |
$error |
A WP_Error holding the reason for the failure (added in WordPress 5.4) |
The error code tells you why it failed. For example, if the username exists but the password is wrong, it is incorrect_password; if the username doesn't exist, it is invalid_username. When the username or password is empty (empty_username / empty_password), this hook does not fire.
Because the hook fires inside wp_authenticate(), it runs not only for the login screen but also for authentication failures through XML-RPC (xmlrpc.php). On the other hand, failed authentication with REST API application passwords is handled by a different hook (application_password_failed_authentication), and wp_login_failed does not fire.
If you sent an email on every failed login, a brute-force attack would land hundreds of emails in your inbox. So we build it like this:
- Count failures per IP address (stored in a WordPress transient)
- Send just one email when the count reaches 5
- Don't resend for the same IP address for 1 hour
Step 1: Create an mu-plugin that reports failed logins
Put the code in an mu-plugin (must-use plugin), not in the theme's functions.php. That way it keeps running when you switch themes, and nobody can deactivate it by mistake from the plugin list in the admin.
Create the wp-content/mu-plugins/ folder (if it doesn't exist) and create login-failure-alert.php inside it with the following content. PHP files directly inside mu-plugins load automatically without being activated.
<?php
/**
* Plugin Name: Login Failure Alert
* Description: Email the administrator when failed logins keep coming from the same IP address.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_login_failed', 'lfa_on_login_failed', 10, 2 );
function lfa_on_login_failed( $username, $error = null ) {
$threshold = 5; // Notify after this many consecutive failures
$window = 900; // Seconds to remember the failure count (15 minutes from the last failure)
$cooldown = 3600; // Seconds before notifying again about the same IP (1 hour)
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
$key = 'lfa_' . md5( $ip );
// Count failures
$count = (int) get_transient( $key . '_count' ) + 1;
set_transient( $key . '_count', $count, $window );
if ( $count < $threshold ) {
return;
}
// Don't send if already notified (prevents a flood of email)
if ( get_transient( $key . '_sent' ) ) {
return;
}
set_transient( $key . '_sent', 1, $cooldown );
$site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
$subject = sprintf( '[%s] Repeated failed logins', $site );
$message = sprintf(
"%d failed logins in a row from the same IP address.\n\n"
. "IP address: %s\n"
. "Last username entered: %s\n"
. "Error type: %s\n"
. "Date: %s\n"
. "Site: %s\n",
$count,
$ip,
sanitize_text_field( $username ),
is_wp_error( $error ) ? $error->get_error_code() : '-',
wp_date( 'Y-m-d H:i:s' ),
home_url( '/' )
);
wp_mail( get_option( 'admin_email' ), $subject, $message );
}
The key points of the code:
- The recipient is the "Administration Email Address" (
admin_email) under Settings > General. set_transient()extends the expiry on every failure, so it keeps counting as long as the next failure comes within 15 minutes of the last one.- You can change the threshold, the window and the resend interval with the three variables at the top.
- It only counts failures; it has no feature to stop (lock out) logins. Ways to stop them are covered in a later section.
Step 2: Also notify on administrator logins (optional)
Notifying the account owner not only on failures but also when an administrator account logs in successfully lets you notice if a leaked password has been used to log in. On a successful login, WordPress fires the wp_login action (its arguments are the username and the WP_User).
Create wp-content/mu-plugins/admin-login-notice.php.
<?php
/**
* Plugin Name: Admin Login Notice
* Description: Email an administrator when they log in.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action( 'wp_login', 'aln_on_login', 10, 2 );
function aln_on_login( $user_login, $user ) {
// Notify only users with administrator capabilities
if ( ! user_can( $user, 'manage_options' ) ) {
return;
}
$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
$site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
$subject = sprintf( '[%s] An administrator account just logged in', $site );
$message = sprintf(
"Username: %s\nIP address: %s\nDate: %s\n\nIf this wasn't you, change your password right away.\n",
$user_login,
$ip,
wp_date( 'Y-m-d H:i:s' )
);
wp_mail( $user->user_email, $subject, $message );
}
You will get an email every time you log in yourself. On sites where administrators log in often, look at how many arrive before deciding whether to keep it.
Step 3: Check that it works
- While logged out (in another browser or a private window), open the login screen
- Try to log in 5 times with an existing username and a wrong password
- Confirm that an email with the subject "Repeated failed logins" arrives at the administrator email address
- Confirm that further failures from the same IP address don't trigger another email for 1 hour
In our test environment, exactly one email was sent on the 5th failure and none from the 6th on. The email looked like this:
Subject: [Site name] Repeated failed logins
5 failed logins in a row from the same IP address.
IP address: 203.0.113.9
Last username entered: admin
Error type: incorrect_password
Date: 2026-10-29 17:23:07
Site: https://example.com/
If no email arrives, WordPress itself may not be able to send mail. See "How to fix WordPress emails not being sent" and set it up to send through SMTP.
Things to watch
Behind a proxy or CDN, the IP address isn’t correct
If traffic goes through a CDN such as Cloudflare or a reverse proxy, $_SERVER['REMOTE_ADDR'] contains the proxy's IP address. In that state, every visitor is counted as the same IP address.
Headers such as X-Forwarded-For can be freely set by the client, so don't trust them as is inside PHP. Configure the web server to put the real IP address into REMOTE_ADDR using headers only from trusted proxies (Nginx's real_ip module, Apache's mod_remoteip).
If you use an object cache
Transients are normally stored in the database, but with a persistent object cache such as Redis they are stored there instead. It works the same either way, but flushing the cache also clears the failure counts and the record of sent notifications.
Notifications alone don’t stop attacks
This code is for noticing. To stop brute-force attacks, combine it with measures like these:
- Limiting login attempts: the "Limit Login Attempts Reloaded" plugin supports lockout email notifications even in its free version. A method without a plugin is explained in "How to limit WordPress login attempts without a plugin".
- Restricting access to
wp-login.php(IP restrictions, Basic authentication): "How to restrict access to wp-login.php" - Automatic blocking on the server: "Setting up fail2ban to block unauthorized access automatically"
- Two-factor authentication for administrator accounts, and long passwords that are hard to guess
Handling the information in the email
The notification email contains IP addresses and the usernames that were entered. It also reveals whether an entered username exists (incorrect_password vs. invalid_username), so be careful where you forward it and who you share it with.
Summary
- When a login fails, WordPress fires
wp_login_failedand passes the entered username and the reason for the failure (WP_Error). - Count failures per IP address with a transient and send a single notification with
wp_mail()when the threshold is reached, so you don't get flooded with email. - Putting the code in an mu-plugin keeps it independent of theme switches and plugin deactivation.
- Behind a proxy, the web server must be configured to pass the real IP address.
- Notifications are for noticing, so combine them with login attempt limits and access restrictions.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)