Stop contact form spam without a captcha
Block contact form spam without a captcha: a hidden honeypot field, a rate limit and simple content rules. Copy-paste HTML, no third-party scripts.
A lot of contact form spam comes from bots that find a <form> on your page and fill in every field they see. A captcha stops many of them, but it also adds friction for real visitors, loads a third-party script and usually needs an entry in your privacy policy. For a small contact or lead form you can usually skip it: a few quiet checks on the server catch the typical bot without your visitors noticing anything. With Formlane those checks run on the endpoint https://smallwerk.com/formlane/api/submit. You add one hidden field to your form; the rest happens on the server. Submissions are stored in Frankfurt and emailed to your account address; messages flagged by the content rules land in a separate Spam tab instead of your inbox.
Steps
- Sign in to Formlane, create a form and copy its access key (
fl_…). - Add the form below to your page and replace
fl_YOUR_ACCESS_KEYwith your key. The key is public by design, so it is fine in your HTML. - Keep the hidden
botcheckcheckbox exactly as it is. People never see it; bots that tick every box give themselves away. - In the form settings, restrict the form to your domain, so forms on other websites can't post to it.
- Send a test message and check your inbox and the Formlane dashboard.
Code
<form action="https://smallwerk.com/formlane/api/submit" method="POST"> <input type="hidden" name="access_key" value="fl_YOUR_ACCESS_KEY"> <input type="hidden" name="subject" value="New message from my website"> <input type="hidden" name="redirect" value="https://example.com/thanks"> <!-- Honeypot: hidden from people, leave it unchecked --> <input type="checkbox" name="botcheck" style="display:none" tabindex="-1" autocomplete="off"> <label for="name">Name</label> <input id="name" type="text" name="name" required> <label for="email">Email</label> <input id="email" type="email" name="email" required> <label for="message">Message</label> <textarea id="message" name="message" rows="5" required></textarea> <button type="submit">Send</button> </form>
Works on any static host (GitHub Pages, Netlify, Cloudflare Pages, Vercel) and with Hugo, Astro or Jekyll. The email field becomes the reply-to address of the notification.
What Formlane checks on every submission
| Check | What it catches | What happens |
|---|---|---|
Honeypot (botcheck; _gotcha and _honey also work) | Bots that fill in or tick every field | A normal success response (HTTP 200 or the usual redirect), so the bot moves on. Nothing is stored, emailed or counted |
| Rate limit: 10 submissions per 10 minutes per visitor and form | Scripts that hammer one form | Further submissions are rejected with rate_limited (HTTP 429) |
| Content rules: 4 or more links, link markup, spam keywords | Link spam and typical spam text | Saved to the Spam tab, not emailed, not counted toward your monthly limit |
| Allowed domains (optional, per form) | Forms on other websites reusing your public key (a script that fakes the header can still get through) | Rejected with origin_not_allowed |
There is no captcha, no AI and no third-party script involved. Your visitors only see a normal form.
Why a honeypot works (and where it doesn't)
A honeypot is a field that real people never see or touch. Simple bots read the HTML, fill in every input and tick every checkbox, so a filled honeypot is a strong sign of a bot. It costs your visitors nothing: no puzzle, no extra click, no script.
It is not a wall, though. A bot written specifically for your site, or a person pasting spam by hand, will leave the honeypot alone. That is why Formlane combines it with the rate limit and the content rules. Messages flagged by the content rules go to a Spam tab you can check instead of being deleted; honeypot hits are not stored at all.
Tips that help with any form backend:
- Don't name the honeypot after its purpose in visible text (no "leave this empty" label that screen readers announce). Hide it with CSS and
tabindex="-1"as in the code above. - Mark required fields with
requiredand usetype="email". This helps real visitors get it right. Bots skip browser checks, so it is no spam filter on its own. - Don't publish your email address next to the form. The form is the point.
- Watch the Spam tab for a few days after launch. If a real message ever lands there, you'll see it.
Optional: send it with JavaScript
For an inline success message, post the form as JSON and keep the honeypot in the form. message in the response is always English, so the snippet shows its own text per error code:
<script>
const MESSAGES = {
limit_reached: "This form can't take more messages this month. Please email us directly.",
rate_limited: "Too many messages in a short time. Please try again in a few minutes.",
origin_not_allowed: "This form only accepts messages from its own website.",
};
const form = document.querySelector('form[action*="formlane"]');
if (form) form.addEventListener("submit", async (event) => {
event.preventDefault();
// Keep the honeypot in the form: FormData skips it while unchecked, a bot that ticks it gets caught.
const data = Object.fromEntries(new FormData(form));
try {
const res = await fetch(form.action, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(data),
});
const json = await res.json();
form.outerHTML = json.success
? "<p>Thanks! Your message has been sent.</p>"
: "<p>" + (MESSAGES[json.error] || "Sorry, that didn't work. Please try again later.") + "</p>";
} catch {
alert("Network error. Please try again.");
}
});
</script>Using React or Vite? The React guide has the same honeypot in a ready-made component.
Other ways to do it (honest comparison)
| Option | Good for | Watch out for |
|---|---|---|
| Honeypot + server-side checks (Formlane and many hosted form backends) | Contact and lead forms where you want zero friction | Targeted bots and manual spam can get through; check the spam folder now and then |
| Invisible or managed challenge (e.g. Cloudflare Turnstile) | Forms that get hit hard despite a honeypot | Loads a third-party script; mention it in your privacy policy. Only stops bots if a server verifies the token (Formlane doesn't today) |
| Classic captcha (e.g. reCAPTCHA, hCaptcha) | High-value targets such as sign-ups or logins | A visible challenge adds friction and can be hard for some visitors; third-party script and privacy policy entry |
| Your own serverless function with your own rules | Full control, custom logic | You build and maintain the checks, storage and email delivery yourself |
Formlane is the right pick if you want a plain HTML form on a static site, spam filtering without a captcha and submissions stored in Frankfurt. If you need file uploads, autoresponders, webhooks or a captcha today, choose a service that has them: Formlane doesn't yet.
Pricing
Free: 250 submissions a month and up to 5 forms. Starter: $8/month for 2,500 submissions. Pro: $19/month for 25,000 submissions. Prices are in USD, plus applicable taxes. Spam doesn't count toward the monthly limit. Once the monthly limit is reached, new messages are rejected until the next month or an upgrade.
Related guides
FAQ
Can I stop contact form spam without a captcha?
For many small contact forms, yes. A hidden honeypot field, a rate limit per visitor and simple content rules catch the typical form bot without any visible challenge. Formlane runs these checks on every submission. If a form is targeted on purpose, a challenge such as Turnstile can help, but it needs a server that verifies its token. Formlane doesn't verify captcha tokens today.
What is a honeypot field?
A form field that is hidden from people with CSS. Real visitors never fill it in; simple bots fill in everything. If the field arrives filled in or ticked, the submission is from a bot. With Formlane, use a hidden, unchecked checkbox named botcheck.
Do bots notice that they were blocked?
Not from the honeypot. Formlane answers a honeypot hit with a normal success response (HTTP 200 or the usual redirect) and stores nothing, so the bot has no reason to retry. Rate-limited submissions get a rate_limited error.
Does spam count toward my monthly limit?
No. Honeypot hits are not stored at all. Messages caught by the content rules go to the Spam tab, are not emailed to you and do not count toward your monthly limit. Once the monthly limit is reached, new messages are rejected until the next month or an upgrade.
Is the honeypot accessible?
Yes, if it is hidden properly: display:none removes it from the page and from screen readers, and tabindex="-1" keeps it out of the keyboard order. Don't put a visible label on it.
Can other websites use my public access key to send spam?
The key only allows submissions to that one form. To stop other sites from using it, restrict the form to your domain in its settings; browsers on other sites are then rejected with origin_not_allowed. A script that fakes the header can still get through; the honeypot, rate limit and content rules still apply to it.
Where are submissions stored?
Submissions are stored in Frankfurt, in a Supabase database (region eu-central-1). The app runs on Vercel and notifications are sent via an email provider; the privacy policy lists all providers.
Get your access key
Free for 250 submissions a month. Submissions stored in Frankfurt.
Get your access key