<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Naveen kumar</title>
    <description>The latest articles on DEV Community by Naveen kumar (@naveen_kumar_4172fd753133).</description>
    <link>https://dev.to/naveen_kumar_4172fd753133</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4205289%2F3aaba5da-04b9-405d-854b-fc895a7564ad.jpg</url>
      <title>DEV Community: Naveen kumar</title>
      <link>https://dev.to/naveen_kumar_4172fd753133</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naveen_kumar_4172fd753133"/>
    <language>en</language>
    <item>
      <title>5,385 spam comments refused in a day, no CAPTCHA. What I learned building bot protection for WordPress</title>
      <dc:creator>Naveen kumar</dc:creator>
      <pubDate>Sat, 24 Oct 2026 20:36:54 +0000</pubDate>
      <link>https://dev.to/naveen_kumar_4172fd753133/5385-spam-comments-refused-in-a-day-no-captcha-what-i-learned-building-bot-protection-for-5f46</link>
      <guid>https://dev.to/naveen_kumar_4172fd753133/5385-spam-comments-refused-in-a-day-no-captcha-what-i-learned-building-bot-protection-for-5f46</guid>
      <description>&lt;p&gt;I run &lt;a href="//pixelgravitas.com"&gt;PixelGravitas&lt;/a&gt;,  a small team that looks after WordPress sites, and one of the sites we look after received 285,000 spam comments, roughly one a minute, most of them published automatically. It was running the security plugin I build. The plugin logged none of it, because nothing in it watched comments at all. Firewall, login protection, malware scanner: all green, while the front of the site filled with spam.&lt;/p&gt;

&lt;p&gt;So I built comment and bot protection into the plugin. Here's what one day looks like now, from the plugin's own daily summary email:&lt;/p&gt;

&lt;p&gt;5,385 spam comments refused&lt;br&gt;
29 hits on the old login address turned away (the login page was moved, so bots still trying the default address get nothing)&lt;br&gt;
3 malicious requests blocked&lt;br&gt;
1 username-enumeration attempt blocked&lt;br&gt;
Real comments still come through: 16 since it was switched on. And that summary is one email, not 5,385 moderation notices.&lt;/p&gt;

&lt;p&gt;This post is about how it decides, what surprised me, and where I'd like your opinion.&lt;/p&gt;

&lt;p&gt;The constraints&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No CAPTCHA. Puzzles punish real people for what bots do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No third-party service. Many spam filters send each comment, with the commenter's name, email and IP, to an outside service to judge. I wanted nothing about visitors to leave the server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't punish people for sharing an address. An office, a college or a mobile network can put hundreds of real people behind one IP. A naive per-IP limit blocks all of them because of one spammer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The rule: refuse only when certain&lt;br&gt;
Most bot protection works like a bouncer: block or allow. The trouble is the grey area. So the plugin works on one rule:&lt;/p&gt;

&lt;p&gt;Refuse only when certain. When unsure, check the browser, not the person.&lt;/p&gt;

&lt;p&gt;Certain it's a bot. Many spam bots post straight to WordPress's comment endpoint without ever loading the page, and many fill in every field they find, including one that people never see. Either of those gets the comment refused before WordPress saves anything.&lt;/p&gt;

&lt;p&gt;Not sure. Instead of refusing, the plugin asks the browser to do a small proof-of-work in the background. A real browser finishes it in a fraction of a second, usually without the person noticing. A script that isn't a real browser can't. Pass, and a signed cookie means you aren't checked again for a day. I call this Smart protection.&lt;/p&gt;

&lt;p&gt;The same check guards the login page, so password bots that post straight to the login form are stopped before WordPress even checks a password.&lt;/p&gt;

&lt;p&gt;The parts I'm happiest with&lt;br&gt;
Rate limits that don't punish shared networks. A real person on a busy network who goes over the traffic limit doesn't get an error. They get the browser check, pass it, and carry on with more room. A flood with no browser behind it gets a tiny page that costs the server almost nothing.&lt;/p&gt;

&lt;p&gt;Limits per person, not just per IP. By default one person (the same name and email from one address) can post 5 comments an hour, and one address 20, whoever is posting. One spammer on a college network doesn't get everyone else refused.&lt;/p&gt;

&lt;p&gt;IPv6 counted per /64. An IPv6 connection comes with a whole block of addresses, and devices change the rest of the address by themselves. Counting per address would let a bot use a fresh one for every request, so limits and lockouts count the block.&lt;/p&gt;

&lt;p&gt;Suspicious signs lower the limit, they don't block. A visitor with no normal browser headers, or one the firewall blocked in the last hour, gets less room before the check kicks in. Nobody is refused just for looking odd.&lt;/p&gt;

&lt;p&gt;The parts that surprised me&lt;br&gt;
Page caching nearly broke it. Most WordPress sites cache their pages. When the protection switched on, cached copies of pages still carried the old comment form, so real people commenting from them looked like bots. The fix: for the first week, comments from old cached pages are held for approval instead of refused, with a cap so a bot flood can't fill the queue. The plugin also clears the cache of common caching plugins once when the protection starts.&lt;/p&gt;

&lt;p&gt;Spam can take down your email. WordPress emails the owner about every comment waiting for moderation. A flood means thousands of emails, and shared hosts cap how many emails a site can send. Once the cap is hit, every email from the site stops: password resets, order confirmations, contact forms. So the plugin never sends an email per refused or held comment. They're counted in one daily summary, like the one above.&lt;/p&gt;

&lt;p&gt;Trackbacks and pingbacks. On almost every site they're only spam, so they're switched off.&lt;/p&gt;

&lt;p&gt;The honest limits&lt;br&gt;
A bot driving a full, real browser can pass the browser check. Every visit then costs it real browser time, and the hidden field, the per-person limits and the lockouts still apply.&lt;br&gt;
It doesn't judge what a human types by hand. Content filters like Akismet look at the comment text itself, so they solve a different problem. Keep comment moderation on, or use one alongside.&lt;br&gt;
JavaScript. With JavaScript switched off, a comment still goes to the owner for approval, but logging in needs JavaScript while Smart protection is on.&lt;br&gt;
Where I'd like your opinion&lt;br&gt;
Full headless browsers can pass. Is there a cheap signal I'm missing that doesn't turn into fingerprinting?&lt;br&gt;
Is a one-day pass sensible, or should it be shorter, or tied to something else?&lt;br&gt;
Login needing JavaScript while this is on: reasonable, or will it bite someone?&lt;br&gt;
Has anyone measured proof-of-work cost on cheap phones? I don't want to punish people on old devices.&lt;br&gt;
The thing I keep coming back to: the spam wasn't clever. It was the most boring kind of bot, posting again and again to a door nobody was watching. "Everything is green" only covers what a tool checks.&lt;/p&gt;

&lt;p&gt;How do you handle spam on the sites you look after?&lt;/p&gt;

&lt;p&gt;SecureLite Security is a free WordPress plugin, with no account and no upgrade nags: &lt;a href="https://wordpress.org/plugins/securelite-security/" rel="noopener noreferrer"&gt;https://wordpress.org/plugins/securelite-security/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>php</category>
      <category>security</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
