Your comment queue woke up, and now every approval email is another casino link, another "great article!" in Cyrillic, another hidden iframe. You already know Akismet is the default answer. You also know Akismet has required a paid licence for any commercial site for a long time, and "commercial" is a broad definition that covers ads, affiliate links, donations, e-commerce, and in practice almost every business website. Paying for Akismet is a fine choice. So is not paying for Akismet. This article is how to do the second without your comment section turning into a spam billboard.
The goal is a layered defence that catches the overwhelming majority of automated comment spam with two free, GDPR-friendly plugins and the Discussion settings that ship with WordPress core. I will also show you the one developer hook worth knowing about for custom rules, and tell you honestly when paying for Akismet still makes sense. This is written for both the site owner who manages their own WordPress install and the developer who wants to understand what each layer actually does.
How comment spam reaches WordPress
Before you block it, understand where it lands. WordPress comment spam arrives through two very different doors. The first is the public comment form at the bottom of any post that accepts comments, which is what wp-comments-post.php handles. A bot fills in the form and POSTs it. The second is programmatic submission through the REST API or XML-RPC, which skips the HTML form entirely. Both paths end up in the same WordPress comment pipeline, and both pass through the same set of checks before WordPress decides whether a comment is approved, held for moderation, marked as spam, or dropped.
The canonical hook in that pipeline is pre_comment_approved. It fires inside wp_allow_comment() after WordPress has done its initial approval checks and before the comment is written to the database. Every anti-spam plugin you have ever seen, Akismet included, hooks into it. Since WordPress 4.9.0 you can return a WP_Error from that filter and WordPress will skip the database insert entirely, which matters because a normal "spam" verdict still stores the row, just with comment_approved = 'spam'. For high-volume attacks, skipping the insert altogether saves database write load.
What modern comment spam actually looks like, in my experience, is a handful of categories:
- Link-drop bots, the classic: generic praise plus two or three backlinks to unrelated commercial sites. Still the majority of volume.
- SEO-farm bots that paste a relevant-looking paragraph scraped from somewhere else, with one hidden link in an author URL or inside a BBCode tag.
- Cyrillic/CJK spam waves that burst in from a single botnet over a few hours, then vanish. Usually keyword-targeted by script.
- Human spam posted through content-farm services. Low volume, high effort, and every automated filter misses these. A human has to moderate them.
Most of this article is about automated spam, which is 95% of the problem. For the last 5%, the answer is moderation, not more plugins.
Built-in WordPress protections
Before you install anything, harden the defaults. These live in wp-admin > Settings > Discussion and they are the single best return on five minutes of work.
- Before a comment appears > Comment must be manually approved. Turn this on during an active attack. Leave it on permanently if you publish rarely and prefer inbox review. Leave it off if you want real-time comment flow on high-traffic posts; the later layers cover you.
- Before a comment appears > Comment author must have a previously approved comment. Pairs well with the previous option. A user's first comment is held; subsequent comments from the same email auto-publish. Cuts inbox noise in half without locking out new readers.
- Comment author must fill out name and email. Not a strong control (bots fill out fields), but it closes the anonymous-submission path and rules out a small class of lazy bots.
- Automatically close comments on posts older than 30 days. This is the single highest-value setting in the panel. Most spam targets old, indexed posts because they have PageRank and no author watching. Closing comments at 30 or 60 days removes the biggest part of your attack surface without touching your current posts. Pick a number that matches how long your articles typically get new legitimate comments.
- Hold a comment in the queue if it contains N or more links. Default is 2. Lower it to 1 if your legitimate commenters rarely link. Most spam contains three or more links, so this rule holds a lot without filtering real conversations.
- Disallowed Comment Keys. This is the field that replaced the old "Comment Blacklist" in WordPress 5.5. Any comment matching a keyword, phrase, email, URL, or IP on this list goes straight to Trash, never reaches the moderation queue, and never emails you.
That last field is worth expanding on because it is the one people misunderstand. The field is backed by the disallowed_keys option in the WordPress database. When a comment arrives, WordPress runs wp_check_comment_disallowed_list() against it, which checks author name, email, URL, comment body (HTML stripped), IP, and user agent against every line on the list. Matching is case-insensitive and matches partial words: if you add poker on a line, "online poker casino" is caught. This is the fastest way to filter a specific ongoing spam wave. If the same Cyrillic word, the same nonsense domain, or the same IP range is hammering you, a single line on this list stops it.
A community-maintained blocklist worth knowing about is splorp/wordpress-comment-blocklist, which ships a list of around 35,000 patterns drawn from years of comment-spam moderation. Copy the contents of blacklist.txt into the Disallowed Comment Keys field and save. It is large, but WordPress is happy with it. Treat it as a starting set that you prune over time.
One important rename for developers who read old tutorials. WordPress 5.5 renamed two options in the comment-moderation path: blacklist_keys became disallowed_keys, and comment_whitelist became comment_previously_approved. At the same time, the function wp_blacklist_check() was deprecated in favour of wp_check_comment_disallowed_list(). Old code calling the deprecated names still works (with a notice), but anything you write today should use the new names.
Honeypot approach with WP Armour
Layer two is a honeypot. A honeypot is a form field that is invisible to humans but present in the page source for a naive bot. If the field is submitted with any value, the submission is spam and is dropped. Honeypots have two killer properties. They create zero friction for legitimate commenters (no CAPTCHA, no puzzle, no math question), and they cost nothing in API calls or external dependencies.
The catch with most honeypots is that sophisticated bots parse the page, notice that the honeypot field has style="display:none" or type="hidden", and skip it. WP Armour – Honeypot Anti Spam handles that by injecting the honeypot field with JavaScript after the page loads, and by using a unique field name per WordPress install. A bot that cannot execute JavaScript never sees the field at all, and therefore fails to submit it, which is the signal WP Armour uses to mark the attempt as spam. The plugin is free, has 300,000+ active installs, was last updated in December 2025 (version 2.3.04), and is GDPR compliant by virtue of making zero external calls.
Installing it is genuinely install-and-forget:
- In
wp-admin > Plugins > Add New, search for "WP Armour Honeypot Anti Spam". - Install and activate.
- There is nothing to configure. The default settings work.
The free version of WP Armour protects the default WordPress comment form, the registration form, BBPress, and a long list of supported form plugins: Contact Form 7, Gravity Forms (non-AJAX and non-multi-step), WPForms, Formidable Forms, Elementor Forms, Fluent Forms, and more. The paid Extended version adds WooCommerce Checkout, Easy Digital Downloads, Ninja Forms, and Gravity Forms AJAX/multi-step, which is worth knowing if your site is one of those.
You will know it worked when the page source of a single post shows no honeypot field after the initial load, but a field with a random name appears in the form after a second or two (you can see it in DevTools > Elements as an input that was inserted by a script). Submit a comment manually and it should go through as normal.
Two honest limitations. WP Armour does not block human spammers, because humans see the form exactly like legitimate commenters do. And it is not compatible with iframe-based comment systems like Jetpack Comments, wpDiscuz, or Disqus, because those replace the native form with an iframe that WP Armour does not control. If you are using one of those, the honeypot layer is handled by the comment system itself and you can skip to the next layer.
Antispam Bee: GDPR-friendly heuristic filtering
Layer three catches what the honeypot missed. Some bots execute JavaScript, some targeted attacks are too clever for field-based tricks, and you still want a fallback. That is what Antispam Bee does. It is a heuristic filter that runs nine different checks against each submitted comment, flags anything that looks like spam, and marks it accordingly. It is free for personal and commercial use with no usage caps, has 700,000+ active installs, was last updated on March 30, 2026 (version 2.11.10), and is the GDPR-friendly default on most European WordPress sites for one reason: it does not require any API key or send comment data to a third party by default.
Installing and configuring it:
- In
wp-admin > Plugins > Add New, search for "Antispam Bee" by pluginkollektiv. - Install and activate.
- Go to
wp-admin > Settings > Antispam Bee. - Enable these for a sensible default set:
- Trust approved commenters (skips all other checks for users who have had a comment approved before; cuts false positives to near-zero for regulars).
- Trust commenters with a Gravatar (optional; only sends an MD5-hashed email to Gravatar, not the email itself).
- Consider the comment time (flags submissions that arrive faster than a human could plausibly type; added in v2.6.4).
- BBCode check (catches forum-spam patterns with
[url=...]syntax). - Validate IP address (local check).
- Use local spam database (cross-references IP/email/URL against stored spam from previous detections on your own site).
- Leave Country blocking off unless you have a clear reason. It sends an anonymized IP to an external IP2Country service and is easy to misuse in a way that blocks legitimate international readers.
- Leave Language filtering off for the same reason. It sends comment text to the franc language-detection service over HTTPS, which is a GDPR grey area for comment bodies, and the false-positive rate on multilingual sites is high.
- Save.
The documented Antispam Bee settings reference explains each check in detail, including which ones make external HTTP calls and which ones stay local. For a site that wants zero external calls, the default configuration above (with the Gravatar option off) is fully local.
You will know it worked when your existing spam queue starts filling with comments that have an Antispam Bee "reason" annotation visible in wp-admin > Comments > Spam, and when genuine comments from known commenters continue to flow without being flagged.
One caveat. Antispam Bee only protects the native WordPress comment form. It does not integrate with Contact Form 7, Gravity Forms, or any other form plugin, and it is not compatible with Jetpack Comments, wpDiscuz, or Disqus (for the same iframe reason as WP Armour). For contact-form spam, your form plugin's own honeypot or validation rules are the right place to act. For comments specifically, Antispam Bee plus WP Armour covers the native-comment case completely.
Akismet: when it still makes sense
Akismet is not the enemy, it is just not free for commercial sites. Akismet's own support page defines "commercial" broadly: running ads (AdSense, Taboola), affiliate links, a live chat plugin, promoting a business, having a business domain, accepting donations, or running e-commerce. The commercial-licence policy is longstanding, not a change from the 5.0 release in July 2022; the 5.0 release itself only added a bot-interaction observation feature. People mostly noticed the licensing rule more clearly when Akismet tightened the signup flow around that period.
If you do run a personal, non-commercial blog, you can still get a free Akismet key by sliding the "pay what you can" slider all the way to zero. That remains genuinely supported and is a reasonable choice for a hobby blog: Akismet's cloud-based service is trained on a massive corpus of cross-site spam patterns and catches things that local heuristics miss.
For commercial sites, Akismet Pro is currently €9.95 per month, billed yearly, for one site (Akismet version 5.6 as of November 12, 2025). That is not expensive for a site that makes revenue. The honest reasons to pay it anyway:
- You want a single, well-known vendor on the record for anti-spam and you do not want to tune local plugins.
- You have a high-volume comment section where the cross-site pattern matching demonstrably outperforms local heuristics.
- You are a freelancer handing a site off to a non-technical client who will not maintain a layered setup, and a single toggle is the delivery constraint.
Reasons not to pay:
- Your commercial site has modest comment volume, and WP Armour plus Antispam Bee plus the Disallowed Comment Keys list cover your actual attack pattern.
- You care about GDPR data-flow minimization and do not want comment metadata leaving the EU by default.
- You want to stay vendor-independent.
Either choice is defensible. The wrong choice is installing Akismet on a commercial site with no paid key and hoping it does not matter, because Akismet's support docs say non-compliance can result in immediate suspension without notice, and a suspended key leaves your site unprotected at the exact moment you thought it was covered.
Closing comments on specific post types or old posts
Sometimes the honest answer is "I do not want comments on this". That is fine. WordPress lets you turn them off at three levels of granularity.
Site-wide, via wp-admin > Settings > Discussion > Default article settings > Allow people to submit comments on new posts. Unchecking this applies to every post created after the change. Existing posts keep their current setting.
Per post type, via code. For example, to disable comments on WooCommerce products:
// In your theme functions.php or a site-specific mu-plugin
add_action( 'init', function () {
remove_post_type_support( 'product', 'comments' );
remove_post_type_support( 'product', 'trackbacks' );
}, 100 );
On old posts automatically, via the Discussion setting already mentioned: "Automatically close comments on posts older than N days". This is the one most sites should enable even when they want new comments on fresh posts. Pick a number that reflects how long conversations on your posts typically stay active (30 days works for most blogs, 90 for longer-form technical content).
What the "disable comments completely" line of thinking misses is that most spam targets old indexed posts. Closing comments at 30 days removes the largest part of the attack surface while keeping the commenting experience alive on the posts where it matters. It is almost always a better default than a site-wide disable.
The developer hook: custom rules with pre_comment_approved
If you need a custom rule that none of the above covers (block any comment containing a specific phrase, block comments from a known problem IP range, require a minimum comment length), the clean place to put it is the pre_comment_approved filter. A small mu-plugin is the right container:
<?php
// wp-content/mu-plugins/comment-custom-rules.php
// Runs before any comment is written to the database.
// Return 1 to approve, 0 to hold, 'spam' to mark as spam,
// 'trash' to drop, or a WP_Error to skip the DB insert entirely.
add_filter( 'pre_comment_approved', function ( $approved, $commentdata ) {
// Already decided by an earlier filter (Akismet, Antispam Bee, etc.)
if ( is_wp_error( $approved ) || 'spam' === $approved ) {
return $approved;
}
$content = isset( $commentdata['comment_content'] )
? $commentdata['comment_content']
: '';
// Drop comments that contain more than 3 URLs entirely.
// preg_match_all returns the URL count; threshold is your call.
if ( preg_match_all( '#https?://#i', $content ) > 3 ) {
return new WP_Error(
'too_many_links',
'Comment contains too many links.'
);
}
return $approved;
}, 20, 2 );
Two details matter here. First, returning a WP_Error skips the database insert, which is supported since WordPress 4.9.0 specifically for anti-spam filters that want to drop traffic without storing it. For low-volume sites that is a micro-optimization; for sites under active attack it cuts database writes significantly. Second, the priority 20 puts your filter after Akismet (which runs at 1) and after Antispam Bee, which means their decisions win if they fired first. That is usually what you want: a defense-in-depth layer that adds your own rules without fighting the plugins.
Use this hook sparingly. Every rule you add is a rule you have to maintain, and every false positive is a reader you silenced without warning. Log your rejections for a week before you set them to "hard drop".
WooCommerce product review spam
WooCommerce product reviews are comments, which means everything above applies to them, with one twist. The field that catches the worst offenders on product reviews is Disallowed Comment Keys plus the "hold comments with N links" rule, because most review spam is thin praise with a link. The honeypot layer (WP Armour) covers the product review form as well for most WooCommerce installs (WP Armour's free version supports WooCommerce Reviews Pro; the Extended version explicitly adds WooCommerce Checkout and other commerce flows).
If you want to require that only verified purchasers can leave reviews, WooCommerce itself has that setting at wp-admin > WooCommerce > Settings > Products > Reviews > Show "verified owner" label on customer reviews. The stricter variant is to only allow verified owners to leave reviews at all, which eliminates most review spam entirely at the cost of losing reviews from users who bought off-site (or received a gift) and wanted to comment. The trade-off is worth it for most shops.
Myths worth burning
A short list of things people try that do not help, and why.
"CAPTCHAs stop all spam bots." They used to. Modern bots solve reCAPTCHA v2 challenges, and reCAPTCHA v3 is a silent score that can misclassify legitimate users. CAPTCHAs also add friction for readers, which is the opposite of what a comment section is for. The WP Armour FAQ says it plainly: bots have learned to solve captchas, so captchas are no longer effective. Honeypots and interaction observation do the work now.
"Disabling comments completely is the easiest solution." It is the nuclear option, and it is overkill when "close comments after 30 days" already removes most of the attack surface. If you actually do not want any comments, site-wide disabling is fine. But do not reach for it because you are tired of spam: try closing old posts first.
"Akismet is the only reliable anti-spam option." No. A layered setup of the built-in Discussion settings, WP Armour, and Antispam Bee covers the overwhelming majority of automated spam on the overwhelming majority of sites, at zero cost and zero external data flow. Akismet is a reasonable choice for personal blogs (free) and for specific commercial cases (see above). It is not the only one.
"One big blocklist catches everything forever." A blocklist is a starting point, not a permanent solution. Spam keywords change, domains rotate, and a stale list blocks legitimate words that drift into the spam corpus by accident. Review your Disallowed Comment Keys list once a quarter and prune anything that looks like a false positive.
When to escalate
Call in help when any of these is true, and collect the list below before you do.
- WP Armour and Antispam Bee are both installed and your spam queue still fills faster than you can clear it. The attack probably bypasses the honeypot layer (human spammers, JS-capable bots) and needs either Akismet Pro or a web server-level rate limit on
/wp-comments-post.php. - PHP workers are saturating during comment spam waves. See the high CPU usage on WordPress playbook; this is the downstream effect of an unmitigated spam flood.
- You deployed a custom
pre_comment_approvedrule and legitimate comments are silently disappearing. Roll the rule back first, review your log of rejections, then deploy a softer version. - Comments are being posted from authenticated accounts you did not create, or post content is being modified by comments. This is not spam, it is a compromise; switch to the incident-response flow in WordPress security hardening.
Collect before you ask:
- The WordPress version, PHP version, and active theme.
- A list of active plugins with versions (
wp plugin list --format=csvorwp-admin > Plugins). - A screenshot of
wp-admin > Settings > Discussionso the current baseline is visible. - A count of spam comments per hour from the last day (
wp comment list --status=spam --format=countvia WP-CLI, or a manual count fromwp-admin > Comments > Spam). - A sample of five to ten spam comments (author name, email, URL, body). Anonymize if needed.
- Whether you are using Jetpack Comments, wpDiscuz, Disqus, or the native WordPress comment form.
- The nginx or Apache access log entries for
/wp-comments-post.phpduring a spam spike, if you have server access.
If the broader context you need is a full security baseline, WordPress security hardening is the companion article that frames comment spam as one slice of an overall hardening checklist, and brute force protection in WordPress covers the other high-traffic endpoints (wp-login.php and xmlrpc.php) with the same layered approach used here.