Your regular readers leave a comment, it lands in Comments > Spam, you approve it, and the next comment from the same person goes straight back to Spam. Approving does not seem to teach WordPress anything.
This came up in a support forum thread this week. All of a blog’s regular followers were being caught, on a site running All-in-One Security, Jetpack and Akismet at the same time. The fix depends on which of those made the call, so first find out who flagged the comment, then change settings.
WordPress core almost never sends a comment to Spam
Most guides skip this part. When a new comment comes in, core runs its own checks in wp_check_comment_data(), and there are only three outcomes:
- Approved (
1) if it passes every check. - Pending (
0) if it hits the link limit, a word in Comment Moderation, or “Comment author must have a previously approved comment”. - Trash if it matches a word in Disallowed Comment Keys. Core puts it in Spam instead only when
EMPTY_TRASH_DAYSis set to0.
After that, every plugin gets a say through the pre_comment_approved filter, which accepts 1, 0, 'spam', 'trash' or a WP_Error. Anti-spam and security plugins can also change the status after the comment is saved.
So if a comment is in Spam and you have not set EMPTY_TRASH_DAYS to 0, a plugin put it there. That narrows the search a lot.
Step 1. List what is sitting in Spam
In wp-admin you can open Comments > Spam. WP-CLI also gives you the IDs you need for the next steps:
wp comment list --status=spam --number=20 --fields=comment_ID,comment_author,comment_author_email,comment_date
You should get a table of the newest spam comments. Note the ID of one comment from a reader you know is real.
Step 2. Check whether Akismet flagged it
Akismet stores its verdict as comment meta, under keys that start with akismet_. List the meta of that comment:
wp comment meta list 1234
Replace 1234 with the ID from Step 1. If you see akismet_result with the value true, Akismet classified it as spam. No akismet_ rows at all means Akismet did not judge it, so look elsewhere.
The same check in SQL, for example in phpMyAdmin (it only reads, but back up the database before you change anything afterwards):
SELECT meta_key, meta_value
FROM wp_commentmeta
WHERE comment_id = 1234
AND meta_key LIKE 'akismet%';
💡 Replace wp_ with your table prefix from wp-config.php.
If Akismet is the one catching your regulars, mark their comments as Not Spam in wp-admin rather than editing the database. Also confirm on its settings page that Akismet is still connected to an account. In the forum thread, the moderator advised keeping Akismet, because it judges what a comment says while security plugins judge how it arrived.
Step 3. Check your security plugin’s comment options
Security plugins often have their own spambot detection that looks at how the comment was submitted, not at its content.
In the thread, the suspect was the All-in-One Security option “Detect spambots posting comments” under Spam Prevention > Comment spam. The readers were commenting through Jetpack’s comment form and Jetpack Reader instead of the theme’s own form, which that kind of check can treat as a bot.
Two more things from the same thread to check if you use AIOS:
- The permanent block list on the AIOS dashboard. Readers whose comments were marked spam may have been blocked there too, so unblock the entries with the reason “spam” for people you know.
- “Completely block access to XMLRPC” under Firewall > PHP rules. Comments from Jetpack Reader arrive through XML-RPC, so blocking it breaks them.
Other security and anti-spam plugins have similar bot-blocking or honeypot options. Turn them off one at a time and ask a reader to post a test comment after each change.
Step 4. Rule out the core lists and EMPTY_TRASH_DAYS
Even when a plugin is the main suspect, these take ten seconds to check:
wp option get moderation_keys
wp option get disallowed_keys
wp config get EMPTY_TRASH_DAYS
Both lists match inside words, and they check the name, email, URL, IP and user agent too, not only the comment text. The Discussion settings docs give the example that “press” will match “WordPress”. A short entry like a two-letter word or part of an IP address can catch real people.
If wp config get says the constant is not defined, the default applies and disallowed comments go to Trash, not Spam. If it is 0, the Trash is turned off and disallowed comments go to Spam, so Disallowed Comment Keys becomes a real suspect.
Also remember how “Comment author must have a previously approved comment” works for guests. It needs a past approved comment with the same name and the same email, so a reader who types their name differently is held as Pending. That is the Pending queue, not Spam, but the two are easy to mix up.
Catch the plugin in the act with a small mu-plugin
If the steps above do not point to anything, log the status before and after every plugin runs. Save this as wp-content/mu-plugins/debug-comment-status.php and make sure debug logging is on (if the log does not appear, see debug.log not created in WordPress):
<?php
/**
* Plugin Name: Debug comment approval
* Description: Logs who changes the status of new comments. Delete when done.
*/
// Runs first: remember the status WordPress core decided on.
add_filter( 'pre_comment_approved', function ( $approved ) {
$GLOBALS['wpxss_core_status'] = $approved;
return $approved;
}, PHP_INT_MIN );
// Runs last: log the final status and every callback on this filter.
add_filter( 'pre_comment_approved', function ( $approved, $commentdata ) {
global $wp_filter;
$callbacks = array();
foreach ( $wp_filter['pre_comment_approved']->callbacks as $priority => $items ) {
foreach ( $items as $item ) {
$cb = $item['function'];
if ( is_array( $cb ) ) {
$cb = ( is_object( $cb[0] ) ? get_class( $cb[0] ) : $cb[0] ) . '::' . $cb[1];
} elseif ( $cb instanceof Closure ) {
$cb = 'closure';
}
$callbacks[] = $priority . ' ' . $cb;
}
}
error_log( sprintf(
'Comment from %s | core: %s | final: %s | callbacks: %s',
$commentdata['comment_author_email'],
is_wp_error( $GLOBALS['wpxss_core_status'] ) ? 'WP_Error' : var_export( $GLOBALS['wpxss_core_status'], true ),
is_wp_error( $approved ) ? 'WP_Error' : var_export( $approved, true ),
implode( ', ', $callbacks )
) );
return $approved;
}, PHP_INT_MAX, 2 );
Ask a reader to post a comment, then check wp-content/debug.log. If core says 1 or 0 and final says 'spam', one of the listed callbacks changed it. The class or function name usually tells you which plugin.
If both values are the same but the comment still ends up in Spam, the plugin changes the status after the comment is saved. Then deactivate the anti-spam and security plugins one by one, with a test comment after each.
💡 Delete the mu-plugin when you are done. It writes commenters’ email addresses to the log.
Get the real comments back
Once the cause is fixed, restore the readers’ comments. Do it in wp-admin with Not Spam, or by ID with wp comment unspam, which prints a line like Success: Unspammed comment 1234. for each one:
wp comment unspam 1234 1235
Or restore every spam comment from one reader’s email address:
wp comment unspam $(wp comment list --status=spam [email protected] --format=ids)
Then empty the rest of the spam folder. If you need to clear comments in bulk, I covered that in how to delete all comments in WordPress. And if comments bring you more spam than conversation, disabling comments is an option too.
TL;DR: As of 2026-11-04, core WordPress only holds comments as Pending or moves disallowed ones to Trash. Spam comes from a plugin unless EMPTY_TRASH_DAYS is 0. Check the comment’s akismet_ meta, your security plugin’s spambot and XML-RPC options, then the moderation and disallowed lists. If you still cannot see who flags them, log pre_comment_approved with the mu-plugin above.

