<?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: Harsimran jeet Singh</title>
    <description>The latest articles on DEV Community by Harsimran jeet Singh (@harsimran_jeetsingh_83e2).</description>
    <link>https://dev.to/harsimran_jeetsingh_83e2</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%2F4231433%2Ff4ab205d-efaf-41cd-b819-582d0b37b67e.jpg</url>
      <title>DEV Community: Harsimran jeet Singh</title>
      <link>https://dev.to/harsimran_jeetsingh_83e2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harsimran_jeetsingh_83e2"/>
    <language>en</language>
    <item>
      <title>How to stop contact form spam</title>
      <dc:creator>Harsimran jeet Singh</dc:creator>
      <pubDate>Sat, 07 Nov 2026 04:27:29 +0000</pubDate>
      <link>https://dev.to/harsimran_jeetsingh_83e2/how-to-stop-contact-form-spam-cbo</link>
      <guid>https://dev.to/harsimran_jeetsingh_83e2/how-to-stop-contact-form-spam-cbo</guid>
      <description>&lt;p&gt;Contact form spam comes from three places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bots that fill in every field&lt;/strong&gt; they find on the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scripts that POST straight to your form's URL&lt;/strong&gt;, never loading the page at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;People and AI tools writing real-looking messages&lt;/strong&gt;: SEO offers, "I noticed your website…" pitches, crypto and loan offers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each defense below stops some of these and misses others. The cheap ones go first.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Honeypot field
&lt;/h2&gt;

&lt;p&gt;Add a field that people never see. Bots fill it in; people leave it empty. If it has a value, drop the submission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hp"&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Leave this empty &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"website_url"&lt;/span&gt; &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.hp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-10000px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a believable name like &lt;code&gt;website_url&lt;/code&gt; or &lt;code&gt;company_fax&lt;/code&gt;. A field called &lt;code&gt;honeypot&lt;/code&gt; is easy for a bot to skip.&lt;/li&gt;
&lt;li&gt;Hide it with a class, not only &lt;code&gt;display:none&lt;/code&gt; inline: some bots skip inputs with inline hiding.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tabindex="-1"&lt;/code&gt; and &lt;code&gt;aria-hidden&lt;/code&gt; keep keyboard and screen reader users out of it. &lt;code&gt;autocomplete="off"&lt;/code&gt; stops password managers and autofill from filling it.&lt;/li&gt;
&lt;li&gt;Reject it quietly: answer as if it worked, so the bot doesn't learn to avoid the field.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stops:&lt;/strong&gt; simple bots. &lt;strong&gt;Misses:&lt;/strong&gt; scripts that POST directly (they only send the fields they know), and anything human-written.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Time check
&lt;/h2&gt;

&lt;p&gt;People take more than a few seconds to fill in a form. Many bots submit instantly. Record when the form loaded and reject anything sent too fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"loaded_at"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"loaded_at"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;loaded_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your server, reject the submission if &lt;code&gt;Date.now() - loaded_at&lt;/code&gt; is under about 3 seconds, or if the field is missing. This needs your own backend, and a bot that waits or fakes the value gets through, so treat it as a filter for lazy bots only.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Rate limits
&lt;/h2&gt;

&lt;p&gt;Limit how many submissions one IP address can send per minute and per hour. It won't stop a single spam message, but it stops one bot from sending hundreds and keeps a flood from filling your inbox or your plan's quota. Most form backends and hosting platforms do this for you; on your own server, put the limit in front of anything that sends email.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A challenge: Turnstile, reCAPTCHA or hCaptcha
&lt;/h2&gt;

&lt;p&gt;A challenge widget checks the browser and gives the form a token. Your server then verifies the token with the provider. This stops scripts that POST directly, since they have no valid token.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cloudflare Turnstile&lt;/th&gt;
&lt;th&gt;Google reCAPTCHA&lt;/th&gt;
&lt;th&gt;hCaptcha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What visitors see&lt;/td&gt;
&lt;td&gt;Usually nothing, sometimes a checkbox&lt;/td&gt;
&lt;td&gt;v2: checkbox and image puzzles. v3: nothing, you get a score&lt;/td&gt;
&lt;td&gt;Checkbox, sometimes image puzzles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;No ad tracking, no cookies for tracking&lt;/td&gt;
&lt;td&gt;Runs on Google and uses Google cookies&lt;/td&gt;
&lt;td&gt;Privacy-focused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Free up to a monthly allowance, then paid through Google Cloud&lt;/td&gt;
&lt;td&gt;Free tier, paid plans for more&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Turnstile is the easiest choice for most contact forms: free, rarely visible, and it doesn't send your visitors to Google.&lt;/p&gt;

&lt;p&gt;Add the widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://challenges.cloudflare.com/turnstile/v0/api.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/contact"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- your fields --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cf-turnstile"&lt;/span&gt; &lt;span class="na"&gt;data-sitekey=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_SITE_KEY"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;strong&gt;verify the token on your server&lt;/strong&gt;. The widget alone does nothing: a bot can skip it and post the form anyway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://challenges.cloudflare.com/turnstile/v0/siteverify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TURNSTILE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// reject the submission&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stops:&lt;/strong&gt; bots and direct POSTs. &lt;strong&gt;Misses:&lt;/strong&gt; people pasting a pitch into your form by hand, and AI agents driving a real browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Content filtering
&lt;/h2&gt;

&lt;p&gt;What gets past everything above is usually written by a person or by an AI tool: a real browser, a real pause, a valid token, and a message nobody wants. To catch it you have to look at what the message says.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rules:&lt;/strong&gt; block messages with more than a few links, or with a link in the name field. Cheap, and spammers adapt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Akismet:&lt;/strong&gt; a spam-checking service from Automattic, built into many WordPress setups. Free for personal sites, paid for commercial ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI classification:&lt;/strong&gt; a model reads each submission and scores how likely it is to be spam. It catches pitches that rules miss, because it judges the message the way you would.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any content filter makes mistakes sometimes. &lt;strong&gt;Quarantine, don't delete:&lt;/strong&gt; hold flagged messages somewhere you can check, and let a real lead through with one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Validate on the server
&lt;/h2&gt;

&lt;p&gt;Whatever you add in the browser, check it again on the server: required fields present, a valid email address, reasonable lengths, and only the fields you expect. Bots don't run your JavaScript, so browser-side checks don't count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to use
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A personal site with a little spam&lt;/td&gt;
&lt;td&gt;Honeypot + rate limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A business site, or spam reaching you daily&lt;/td&gt;
&lt;td&gt;Honeypot + Turnstile + rate limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spam that looks hand-written (SEO and sales pitches)&lt;/td&gt;
&lt;td&gt;Add content filtering, with a quarantine you check weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forms where a lost lead costs money&lt;/td&gt;
&lt;td&gt;All of the above, and never auto-delete&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Order matters for cost too: let the honeypot and Turnstile reject bots first, so only messages from real browsers reach the content filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing this with Formkove
&lt;/h2&gt;

&lt;p&gt;If you'd rather not build it, a form backend does this for you. With &lt;a href="https://formkove.com/?ref=devto" rel="noopener noreferrer"&gt;Formkove&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits&lt;/strong&gt; are on for every form: 10 submissions per minute and 30 per hour per IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The AI spam filter&lt;/strong&gt; checks every submission before anything is sent, on every plan including Free. Spam is held in your dashboard, and &lt;strong&gt;Not spam, deliver&lt;/strong&gt; sends a false positive on to its destinations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honeypot&lt;/strong&gt;: set a field name on the form's &lt;strong&gt;AI &amp;amp; Spam&lt;/strong&gt; page and add the hidden input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turnstile&lt;/strong&gt;: paste your own site and secret keys; Formkove verifies the token on every submission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Submissions the AI flags still count toward your monthly limit. Submissions stopped by the honeypot or Turnstile don't, so turn both on if a form attracts a lot of bots. Setup steps are in the &lt;a href="https://formkove.com/docs/spam-protection/?ref=devto" rel="noopener noreferrer"&gt;spam protection docs&lt;/a&gt;, and the free plan includes 300 submissions a month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://formkove.com/blog/stop-contact-form-spam/?ref=devto" rel="noopener noreferrer"&gt;formkove.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>html</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
