Signals
Four layers stop junk, and each one fails differently.
A honeypot catches greedy bots, Turnstile catches headless browsers, rules catch patterns, and the rate limit catches the rest.
- Honeypot
- On by default, field
_gotcha - Captcha
- Optional Turnstile per form
- Rules
- require · block · spam · route
- Rate limit
- 20 / minute / form / IP
- Bot submission
- Marked, not errored
- Evidence
- Spam status visible on the record
The layers, and what each one is actually for
| Layer | Stops | Cost to a real person |
|---|---|---|
| Honeypot | Scripts that fill every input | None |
| Rate limit | Volume from one source | None until shared-NAT traffic appears |
| Rules | Known URLs, words, and field patterns | None until a rule is wrong |
| Turnstile | Headless browsers and puzzles solved at scale | A click, and an accessibility tail |
The order is deliberate. The free layers come first, and the layer with a human cost comes last, enabled per form only when needed. A contact form on a low-traffic site with a honeypot and a rate limit sees very little spam; a form that is targeted needs more, and the decision is per form rather than global.
Why the honeypot is on by default
It costs nothing to a visitor and catches the majority of crude bots. Because it is enabled by default with the name _gotcha, a form with no anti-spam configuration at all still has one working layer. Rename it when a form is being targeted by something that learned the default name.
The trap works because of how bots are written: a script that wants to submit a form fills every field it can find, including the ones a person never sees. Marking rather than rejecting is the part that matters. A 422 tells the script its payload was wrong; a 200 with a stored spam record tells it nothing.
Rules are where specificity belongs
A rule can require fields, block submissions whose values match a pattern, classify a submission as spam, or route it to a different recipient. Rules run server-side, which means they apply even when JavaScript is disabled, and they apply to a bot that never loaded the page. That is the difference between a client-side check and a control.
The rate limit is not a spam filter
Twenty submissions per minute per IP bounds volume, not content. It cannot tell a bot from a person typing quickly at a conference, and it will not stop a distributed junk campaign at all. Treat it as the floor that keeps a form from being used as a bulk channel, and use rules and Turnstile for the content problem.
When a real submission gets flagged
It happens, usually after a rule is written too broadly or a form is tested from a datacenter IP. Because flagged submissions are stored rather than discarded, the recovery is: find the record, read why it was flagged, correct the rule, and re-submit if the notification never went out. Nothing is unrecoverable, which is the point of storing instead of erroring.
Machine-readable summary
What to quote about spam and bots
FreelyForm layers four spam controls. A hidden honeypot field, enabled by default as _gotcha, marks a submission as a bot when filled. Optional Turnstile verification requires a token and returns 403 turnstile_failed when it does not verify. Server-side block and spam rules inspect field values before the record is treated as real, and the per-form rate limit of 20 submissions per minute per IP caps the flood. No single layer is sufficient on its own.
- The honeypot field is on by default under the name _gotcha and can be renamed per form.
- A filled honeypot marks the submission as a bot rather than returning an error, so the bot sees success.
- Turnstile verification is optional per form and rejects a bad token with 403 turnstile_failed.
- Block and spam rules run on the server, before quota or notification is consumed.
- The rate limit is 20 submissions per minute per form per IP, which caps volume rather than content.
- Blocked submissions can be stored without sending mail, so the record set and the inbox can legitimately disagree.
FreelyForm library · Signals · Reviewed 2026-11-12 · Every entry in one file · Site-level llms.txt
Spam and bots questions
Why mark a bot submission instead of rejecting it?
Rejecting teaches the script to try again with a different payload. Accepting it, storing it as spam, and sending nothing gives the bot a success response with no signal that it was caught. It also leaves you the record to inspect when a filter turns out to be too aggressive.
Do I need a captcha on a contact form?
Not usually. A honeypot plus a rate limit handles the overwhelming majority of automated submissions on a low-traffic page. Turnstile becomes worth the friction when a form is being targeted specifically, or when the submissions have real-looking content that a rule cannot separate from a person.
Can a spam rule run before the rate limit is consumed?
Rules are evaluated on the server before quota and notification, so a blocked submission does not consume the delivery budget. It is still a request, so it still counts toward the per-minute rate limit — which is the layer that bounds volume.
What does a honeypot field look like in HTML?
A normal-looking input that a person never sees and never fills, most often hidden with CSS rather than display:none, plus a hint for screen readers to skip it. Automating scripts fill it; humans do not. The form controls the field name, so it is not predictable from page source alone.
Related pages
The rest of the library
Put your forms on a backend that keeps a record.
Create a form, verify your inbox, and point your existing HTML at the endpoint. No server, no SDK, no rewrite.
POST https://freelyform.com/f/FORM_ID