All posts

A contact form without a CAPTCHA: what actually stops spam

There is no CAPTCHA on our contact page. Here is what runs instead, what it caught when we tested it, what it lets through and why a puzzle in front of the send button is usually the most expensive option a small business can pick.

A form with one extra field that stays invisible to people, next to the title of the post

More than half of all web traffic is now machines. Imperva counted 53 percent automated requests for 2025, up from 51 percent the year before. There is still no CAPTCHA on our contact page.

That was a decision, and this is the reasoning behind it: what the form does instead, the test we ran against it and the part that stays open.

What a CAPTCHA costs

A puzzle in front of the send button looks like a small thing. It carries three separate bills.

The first one lands in court. Plaintiffs filed 3,117 federal website accessibility lawsuits in 2025, 27 percent more than the year before, concentrated in New York, Florida and Illinois. That is 36 percent of all ADA Title III filings, counted by Seyfarth Shaw in March 2026. Courts and the Department of Justice measure against WCAG 2.1 AA, and a visual puzzle sits right on top of the first success criterion. The W3C put its own view on the record in 2021 in a note titled "Inaccessibility of CAPTCHA": a design pattern that expects several attempts as the normal case is exclusionary by design. Google's own instructions for the audio alternative in reCAPTCHA v2 run to more than eight steps.

The second one lands with your privacy program. reCAPTCHA sends data about your visitor to Google before that visitor has agreed to anything. Under California law that traffic is a disclosure you have to account for, and your privacy policy has to say so. If any of your visitors sit in the EU, the bar is higher still: an Austrian federal court held in 2024 that reCAPTCHA is not technically necessary, which puts it behind a consent prompt. Google itself changed its role on 2 April 2026 and now acts as a processor, which clears up part of the question and leaves the transfer to the US open.

The third one lands in your pipeline. Every element in front of the send button costs submissions. At five inquiries a month you notice a lost one. At five hundred it disappears into the average. Small businesses live in the first case.

What runs in our form instead

Piece What it does Where it comes from
Honeypot field An invisible input. Anything in it means the submission is dropped Form plugin, field type honeypot
One-time token Every form carries a token tied to the session. Missing or wrong means the request stops Grav, form-nonce
Replay block The same form id is refused the second time refresh_prevention: true
Fixed choices Two of the answers are select fields with a fixed set of values. Anything else fails validation field type select with options
Required consent box A checkbox that has to be ticked field type checkbox with required
Server-side validation Every rule runs on the server. What happens in the browser never decides acceptance Grav, validate
Timestamp, since 27 Sept The form carries the signed time the page was served. Submitting in under three seconds or tampering with the value earns points our own plugin, field type zeitstempel
Content check, since 27 Sept Short links, the opt-out lines of bulk senders, typical fraud and link-selling phrases, known senders and similarity to messages already set aside earn points. At three points the message goes into quarantine our own plugin, step spamcheck

Eight checks, and none of them asks the visitor for anything. The cost to a human being is zero seconds of waiting, zero extra clicks and zero data handed to a third party.

The honeypot in detail

Two lines in the page header define it:

- name: hinweis
  type: honeypot

What comes out in the markup is an input with four properties that belong together:

<input aria-hidden="true" tabindex="-1" autocomplete="off"
       style="visibility:hidden;position:absolute;height:1px;width:1px"
       class="form-honeybear" name="data[hinweis]" value="" />

aria-hidden keeps it out of the screen reader order, tabindex="-1" keeps it out of the tab order, autocomplete="off" keeps the browser from filling it in and the style keeps it off the screen. A person meets this field on none of those four paths. A script that reads the source and fills everything in meets it immediately.

One detail matters more than it looks: a honeypot hidden with display:none gets recognized by some autofill tools. The combination of visibility:hidden and a one-pixel box is the more common answer, because it still looks like a regular field to the software you are trying to catch.

Measured

Descriptions are cheap, so we sent a submission to our own form with every required field, a valid token and the honeypot filled in. The server answered:

HTTP 200
<div class="notices error red"><p>Are you a bot?</p></div>

The submission was dropped, nothing was stored and no notification went out. The check works.

Something else showed up in the same test: the message is in English on a site that runs in German, English and Spanish. It comes out of the form plugin as a fixed string and never passes through the language files. A human sees it rarely, and one false positive is enough. It is on our list now.

What gets through

The honest part, and it is short.

Targeted attempts get through. Anyone who looks at a form by hand spots the honeypot in two minutes and leaves it alone next time. The protection works against software that fills in forms blindly across thousands of sites, which is where the volume is.

AI agents are a new case. Imperva describes them as a category of their own for 2026: systems that carry out tasks for people and render pages the way a browser does. An agent like that sees the invisible field exactly as little as a human does.

Hand-typed outreach gets through the first time. The agency in another time zone offering to sell you links is submitting a technically correct inquiry. Once we mark it as spam, the filter knows the sender and the wording, and the second one lands in quarantine.

We do not know how many attempts the honeypot turns away, and we like the reason: we do not count them. Keeping that statistic would mean logging and storing requests, which is more data processing than the question is worth.

We did count what made it through, and we got that count wrong. Until 27 September this post said there were thirteen real inquiries in the folder. That was false. On 19 September the folder held thirteen submissions: four of our own test runs and nine pieces of spam or fraud. All nine had passed the six checks that were running at the time. They included link-selling offers, a B2B database supposedly shutting down, an inheritance story and a short link to an adult dating page. We still have not had a single complaint from anyone who could not submit the form.

What changed on 27 September

Those nine messages showed us where the gap is. They came from software that renders the page like a browser or from people, and both fill in the form exactly as intended. What gave them away was the text: short links, the opt-out line of a bulk sender, the same senders again and again.

So steps 1 and 3 from the table below are now live. Every message collects points for signs like these and for being sent suspiciously fast. A single link never counts, because prospects mention their current site. At three points the message goes into quarantine. The sender sees the same confirmation as everyone else and learns nothing about what tripped the filter. If a real inquiry lands there, one click brings it back, and its sender goes on a list the filter lets through from then on. Anything in quarantine deletes itself after eight months.

The filter learns as it goes. When it is very sure, it puts the sender on a blocklist, and the same happens whenever we mark a message as spam by hand. It also compares every new message with everything in quarantine, so the same pitch under a new name and a new address stands out by similarity.

We measured this too. We sent all eleven spam and fraud messages that had arrived by 26 September through the form again, along with four inquiries we wrote the way a prospect would. Ten of the eleven went straight to quarantine. The eleventh, a politely worded link swap offer with no link in it, got through. After one click on "mark as spam" the filter caught the same message the second time. All four inquiries landed in the inbox.

We wrote the filter's lists with those eleven messages in front of us, so expect a lower hit rate on new spam. For the quarantine we store each message as it arrived along with the reasons. We still store no IP addresses.

What we add under pressure

In this order, because every step costs more than the one before it:

Step Measure Cost to the visitor
1 Timing check, set aside anything submitted suspiciously fast none
2 Rate limit per IP address per hour in the web server none
3 Content checks for links and known patterns none, we pull false positives back out of quarantine
4 A CAPTCHA without a puzzle, Friendly Captcha or Altcha a short computation in the background
5 A classic image puzzle time, frustration and lost inquiries

Step 5 is listed for completeness. Since 27 September we run steps 1 and 3. For step 2 the volume is too small: eleven messages in eight weeks.

What you can do today

  1. Look at what your form loads from third parties. View the page source: a call to google.com/recaptcha means a connection to Google on every page view, including visitors who never submit anything.
  2. Check whether a honeypot field exists. The common form plugins ship with one, and it is often just switched off.
  3. Send yourself a test inquiry and check whether it arrives, where it lands and how long it takes. Forms fail silently.
  4. Count your inquiries over the last three months. Below twenty a month, almost no CAPTCHA is worth a single lost lead.
  5. If a CAPTCHA has to stay, put it in your privacy policy and pick one that works without a visual puzzle.

If you are unsure what your form sends where: drop us a line with the address of your site. We will read the source and tell you what is in there. That part is free.

Sources

How current this post is

Corrected: none of the first thirteen submissions was a real inquiry, the post said the opposite. Added a timestamp, content checks and a quarantine to the form.

Common questions about form spam

What is a honeypot field?

An extra input in the form that stays invisible to people and looks like any other field to software. A script that fills in every field fills in this one too, and anything that arrives in it gets thrown away. A visitor never sees it, a screen reader skips it and the tab key does not reach it.

Is a CAPTCHA required for a contact form?

No law requires one. What the law asks for is that you protect the data you collect and that your site works for people with disabilities. A form that validates on the server, rejects bulk attempts and publishes nothing does the first job. Skipping the puzzle does the second one.

Does an accessibility widget or a CAPTCHA plugin protect me from an ADA claim?

A widget on its own has not protected anyone. Of the companies sued in August 2026, 134 had a third-party accessibility overlay running at the time. What holds up is documented conformance with WCAG 2.1 AA: an audit, the fixes, monitoring and dated evidence. A CAPTCHA works against you here, because a visual puzzle is one of the clearest barriers a site can put in front of a person.

Does a honeypot stop AI agents?

Some of them. An agent that reads the page source and fills in the fields it finds walks straight into it. An agent that renders the page like a browser and only touches visible inputs gets through. Against the second kind you need checks on timing and content, and in the end a human reading the message.

What about rate limits and blocklists?

A limit per IP address per hour belongs in the web server and costs nothing inside the form. It pays off once many attempts arrive in a short time. We have kept our own sender blocklist since 27 September: every message we mark as spam puts its sender on it, and the next message from there lands in quarantine.

What happens to messages in quarantine?

They stay there without a notification and outside the list of inquiries. The sender sees the same confirmation as with any inquiry. If a real inquiry lands there, one click brings it back. Entries delete themselves after eight months.

If I do need a CAPTCHA, which one?

One that runs without a puzzle. Friendly Captcha computes a small task in the background and is certified against WCAG 2.2 level AA, Altcha does the same as open source on your own server, Cloudflare Turnstile is the convenient middle ground. Before picking any of them, count how much spam you actually get.