# Stop contact form spam: honeypot vs reCAPTCHA vs Turnstile

> How to stop contact form spam: honeypots, time traps, rate limits and content filters vs reCAPTCHA, hCaptcha and Turnstile, with working code you can copy.

Source: https://formsmtp.com/guides/contact-form-spam-protection · Last updated November 2, 2026

## Why it matters more than it looks

Bots find forms by crawling and post to them automatically. If your form sends email through your own account, every junk submission also uses your sending quota and puts spam in your inbox. That can hurt your sender reputation too. So stop bots before the email is sent, not afterwards with a mail filter.

## The methods compared

| Method | How it works | Visitor effort | Misses |
| --- | --- | --- | --- |
| Honeypot | A field hidden with CSS. People leave it empty; naive bots fill every field. | None | Bots that render CSS or target your form specifically |
| Time trap | Record when the page loaded. A submission within a couple of seconds isn’t a person typing. | None | Slow or scripted bots that wait |
| Rate limit | Cap submissions per IP per minute and hour. | None | Bots spread over many IPs |
| Content filter | Score the text: many links, link markup, known spam phrases. | None | Short, plain spam. Can misfire on real messages. |
| Cloudflare Turnstile | A widget that runs checks in the background; your server verifies its token. | Usually none | Human spammers |
| hCaptcha | A widget that may show a challenge; your server verifies its token. | Sometimes a challenge | Human spammers |
| reCAPTCHA v2 | The “I’m not a robot” checkbox, sometimes with image challenges. | A click, sometimes more | Human spammers |
| reCAPTCHA v3 | No widget; returns a score from 0.0 (bot) to 1.0 (human) for your server to judge. | None | You pick the threshold; Google suggests starting at 0.5 |

## Honeypot vs CAPTCHA

Start without a CAPTCHA. The W3C’s note on CAPTCHA accessibility concludes that traditional CAPTCHAs “continue to be challenging for people with disabilities”, and recommends: “if a honeypot suffices, use a honeypot until evidence of robotic attacks dictates something else.”

A honeypot plus a time trap plus a per-visitor rate limit stops most automated spam and costs visitors nothing. Add a CAPTCHA when the spam that remains is clearly automated. A CAPTCHA can’t stop a person typing spam by hand, and nothing on this page can, so keep a content filter with a way to review what it catches.

## Turnstile vs hCaptcha vs reCAPTCHA

- **Cloudflare Turnstile** describes itself as a CAPTCHA alternative that works “without showing visitors a CAPTCHA”. It doesn’t require your site to use Cloudflare’s CDN. The free plan lists unlimited challenges and up to 20 widgets. Tokens last 300 seconds and can be verified once.
- **hCaptcha** uses the same pattern: a `div` with your site key, a token posted as `h-captcha-response`, and server-side verification. Tokens are single-use and expire after 120 seconds by default.
- **reCAPTCHA** v2 posts `g-recaptcha-response`. v3 returns a score instead of a challenge. Tokens are valid for two minutes and can be verified once.

All three only work if **your server** checks the token with the provider. A widget on the page with no server check stops nothing, because a bot just skips the widget.

## Build it yourself

A honeypot and a time trap in a few lines. Name the honeypot something a bot wants to fill, and tell the bot it worked so it doesn’t adapt:

```js
<!-- In the form: a field people never see -->
<input type="text" name="website_url" style="display:none" tabindex="-1" autocomplete="off">
<input type="hidden" name="loaded_at" id="loaded_at">
<script>document.getElementById("loaded_at").value = Date.now();</script>

// On the server (Express)
app.post("/contact", express.urlencoded({ extended: false }), async (req, res) => {
  const tooFast = Date.now() - Number(req.body.loaded_at) < 2000;
  if (req.body.website_url || tooFast) return res.redirect(303, "/thanks"); // look successful, send nothing
  // ...verify the CAPTCHA token here if you use one, then send the email
});
```

Then verify the CAPTCHA token, if you use one:

```js
// Server-side check, required: the widget alone proves nothing.
const r = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    secret: process.env.TURNSTILE_SECRET,
    response: req.body["cf-turnstile-response"],
    remoteip: req.ip, // optional
  }),
});
const { success } = await r.json(); // tokens last 300 seconds and verify only once
```

Add a per-IP rate limit (most frameworks have middleware for it), and log what you block so you can spot false positives.

## What FormSMTP does

**Using FormSMTP?** If your form posts to FormSMTP, these layers run on every submission, in this order. You don’t need to build them.

1. **Per-visitor limits:** 5 submissions a minute and 10 an hour per IP by default (the hourly number is yours to change).
2. **Honeypot:** if `_gotcha`, `_honey` or `_hp` has a value, the submission is dropped and the bot gets a normal success response.
3. **CAPTCHA, optional:** Cloudflare Turnstile, hCaptcha or Google reCAPTCHA with **your own keys**. FormSMTP verifies the token server-side. reCAPTCHA v3 scores under 0.5 fail.
4. **Duplicates:** the same submission from the same visitor within 10 minutes is sent once.
5. **Time trap:** with a `_t` field holding the page-load time, anything sent within 2 seconds is held.
6. **Content score:** more than three links, link markup, known spam phrases or a URL in the name field gets the message held.

Held submissions appear under *Logs → Blocked* with the reason. If one was a real person, click **Send anyway** within 7 days. The duplicate check, time trap and content score are the *Spam filter* switch in *Settings → Spam protection*, on by default.

## 1. Create a Turnstile widget

In the Cloudflare dashboard, add a Turnstile widget for your site’s hostname. Copy the **site key** (public, goes in the page) and the **secret key** (private).

## 2. Add the secret to FormSMTP

In *Settings → Spam protection*, set **CAPTCHA** to *Cloudflare Turnstile* and paste the secret key. From then on, submissions without a valid token are refused with “Please complete the CAPTCHA.” Do this step after the widget is live on your page.

## 3. Add the widget to your form

Turnstile adds a hidden `cf-turnstile-response` field inside the form, which FormSMTP reads and keeps out of the email:

```html
<form action="https://app.formsmtp.com/api/v1/form/YOUR_FORM_KEY" method="POST">
  <label>Name <input name="name" required></label>
  <label>Email <input name="email" type="email" required></label>
  <label>Message <textarea name="message" rows="5" required></textarea></label>

  <!-- Honeypot and time trap: free, invisible, catch most bots -->
  <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
  <input type="hidden" name="_t" id="loaded-at">

  <!-- Cloudflare Turnstile: adds a hidden cf-turnstile-response field to this form -->
  <div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_SITE_KEY"></div>

  <button type="submit">Send message</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script>document.getElementById("loaded-at").value = Date.now();</script>
```

For hCaptcha or reCAPTCHA, swap the `div` and script for theirs. FormSMTP reads `h-captcha-response` and `g-recaptcha-response`. The *API & code* page in the dashboard generates the snippet for the provider you picked.

New to form endpoints? Start with the [HTML contact form guide](https://formsmtp.com/guides/html-contact-form) or [how form SMTP works](https://formsmtp.com/form-smtp). [Create a free account](https://app.formsmtp.com/#/signup) to try it.

## Sources

Checked on the vendors’ own pages on November 2, 2026.

- [W3C: Inaccessibility of CAPTCHA (Group Draft Note)](https://www.w3.org/TR/turingtest/)
- [Cloudflare Docs: Turnstile client-side rendering](https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/)
- [Cloudflare Docs: Turnstile server-side validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/)
- [Cloudflare Docs: Turnstile plans](https://developers.cloudflare.com/turnstile/plans/)
- [hCaptcha Developer Guide](https://docs.hcaptcha.com/)
- [Google reCAPTCHA v3 docs](https://developers.google.com/recaptcha/docs/v3)
- [Google reCAPTCHA: Verifying the user’s response](https://developers.google.com/recaptcha/docs/verify)

## FAQ

### What’s the best way to stop contact form spam?

Layer cheap, invisible checks first: a honeypot field, a time trap and a per-IP rate limit. Add a content filter you can review. Add a CAPTCHA such as Turnstile only if automated spam still gets through.

### Honeypot or CAPTCHA?

Start with a honeypot. It costs visitors nothing and has no accessibility cost. The W3C recommends using a honeypot when it’s enough, and adding more only when bots get past it.

### Is Cloudflare Turnstile free?

Cloudflare’s Turnstile plans page lists a free plan with unlimited challenges and up to 20 widgets per account. You don’t need to put your site behind Cloudflare to use it.

### Does a CAPTCHA widget work without server-side verification?

No. Bots can submit the form without loading the widget. Your server, or your form backend, must send the token to the provider’s siteverify endpoint and reject failures.

### Will a honeypot block screen reader users?

Not if it’s hidden with display:none and tabindex="-1", as in the examples here, because assistive technology skips it too. Avoid hiding it by moving it off-screen, which some screen readers still read out.

## More guides

- [Gmail 535-5.7.8 Username and Password not accepted: fix](https://formsmtp.com/guides/gmail-smtp-username-password-not-accepted)
- [535 5.7.139 SmtpClientAuthentication is disabled: fix](https://formsmtp.com/guides/microsoft-365-smtp-auth-disabled)
- [SMTP connection timed out (ETIMEDOUT): blocked port fix](https://formsmtp.com/guides/smtp-port-blocked-connection-timeout)
- [SMTP error codes explained: 421, 535, 550, 554 and more](https://formsmtp.com/guides/smtp-error-codes)
