In short
A contact form without tracking or spam needs only HTML and a small program on the server. A hidden field, a signed timestamp and a count of requests per network address make automated submissions harder, with no cookies, CAPTCHA or script in the browser. The server checks everything and sends the inquiry only to your mailbox.
Key takeaways
- A form works without JavaScript: the HTML sends the entries to a program on the server, which checks everything and returns any errors as a new page.
- Several checks work together against spam: the origin of the request, a field hidden from people, a signed timestamp and a count per network address.
- According to ENISA, a plain hash of an IP address can be reversed in about two minutes. A counter needs a secret key that changes regularly.
- An automatic confirmation sent to the address someone entered exposes a form to attacks on other people’s inboxes. If the email stays with the site operator, that route is closed.
- Anyone who fails a check needs to know: after errors or too many requests, the entries stay in place and the page names another way to reach you.
Why can a contact form do without a CAPTCHA?
A contact form offers bots little to gain: no account and no payment data, just an email sent to you. In our view, checks on the server are enough for that. A CAPTCHA, on the other hand, shuts some people out and often brings in a third-party service.
By “without tracking,” we mean a form that sets no cookies, runs no analytics scripts and sends no requests to third-party servers, either when the page loads or when the form is submitted. A CAPTCHA is a test meant to tell people from programs. On a simple inquiry form, it has three drawbacks:
- Some people fail it. The World Wide Web Consortium (W3C) writes that a CAPTCHA often prevents people with disabilities from completing the task (W3C note on CAPTCHA, draft of Dec 16, 2021 (external site)).
- The protection is limited. Researchers at the University of California, Irvine, studied reCAPTCHA v2 for 13 months with more than 3,600 people. They concluded that it has “an immense cost and no security” (Searles et al., preprint (external site)).
- A third-party service loads with it. According to Google, reCAPTCHA sets its own cookie (Google’s reCAPTCHA FAQ (external site)) and loads its script from Google’s servers (reCAPTCHA v3 guide (external site)). When we measured on November 9, 2026, the two scripts came to about 357,000 bytes gzipped. The HTML file of our contact page, also gzipped, was 9,667 bytes. For its cloud product, formerly reCAPTCHA Enterprise, Google commits to using the submitted data only for that service and not for personalized advertising (Google Cloud Service Terms (external site)). The script still runs in your visitors’ browsers.
Services without puzzles, such as Cloudflare Turnstile, also run a script in the browser (Turnstile setup guide (external site)).
There’s a serious case for CAPTCHAs, too. Switzerland’s National Cyber Security Centre (NCSC) recommends them for forms so attackers can’t abuse those forms to flood other people’s inboxes with confirmation emails (NCSC weekly review, week 6/2026 (external site)). If your form sends no email at all to the address someone entered, that attack route is closed. The section on emailing strangers explains how. For user accounts, sign-ups or forms that attackers deliberately target, a CAPTCHA may still be the right choice, in our view.
Done when you know what your form triggers: just an email to you, or also emails to other addresses, accounts or downloads.
How does a form work without JavaScript?
The HTML sends the entries by POST to a small program on the server. The program checks every field, rebuilds the page with error messages where needed, and redirects to a thank-you page after submission. None of this needs a script in the browser.
The action attribute names the program’s address, and method="post" sends the fields in the body of the request (MDN on form data (external site)). The program can be a function at your host, such as a Bunny Edge Script (external site). You don’t need your own server or a database.
<form method="post" action="/form/send">
<label for="name">Name</label>
<input id="name" name="name" required autocomplete="name">
<label for="email">Email</label>
<input id="email" name="email" type="email" required autocomplete="email">
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<!-- Trap for bots: moved out of view with CSS, pick your own names -->
<div class="trap" aria-hidden="true">
<label for="company-url">Please leave this empty</label>
<input id="company-url" name="company-url" tabindex="-1" autocomplete="off">
</div>
<button>Send</button>
</form>Three things belong on the server:
- Check every field again. Checks in the browser can be bypassed (OWASP on input validation (external site)). They help people fill in the form, but they don’t replace a check on the server.
- Return errors as text. The page comes back with a list of errors above the form, and every entry is back in its field.
- Redirect after submission. With status 303, the browser fetches the thank-you page as a new request (RFC 9110, Section 15.4.4 (external site)). Reloading then shows only that page and doesn’t send the inquiry a second time.
The Content-Security-Policy header can limit forms to your own address with form-action 'self' (MDN on form-action (external site)). If you ship no JavaScript at all, you can forbid scripts there entirely. Our explainer on Core Web Vitals covers what third-party scripts mean for your page’s responsiveness. What else belongs to the technical foundation of a business website is in our guide to technical SEO.
Done when the form works fully with JavaScript turned off and errors appear as text ahead of the fields.
How do you spot bots without a CAPTCHA?
Check every request on the server in several layers: Does it come from your website, is the hidden field empty, is the signed timestamp valid, and is the number of requests under your limit? Any single layer can be bypassed. Together they make mass submissions laborious.
| Check | What it does | Limitation | Common mistake |
|---|---|---|---|
| Origin of the request | The server accepts only requests whose Origin header shows they come from your own website. | Programs can set the header themselves. The check mainly keeps out other websites. | Comparing only the start of the address. Someone else’s domain that begins with your name would then pass. Compare the full address. |
| Hidden field (honeypot) | People don’t see the field. If a program fills it in, the server discards the request. | Bots written for exactly your form leave it empty. | Hiding the field with CSS alone. Without aria-hidden and tabindex="-1", keyboard and screen reader users end up in it. |
| Signed timestamp | Proves that the form came from the server and when it was issued. If the server remembers used timestamps, each one works only once. | A bot can wait and replicate the process. | Writing the time into the form without a signature, or setting a minimum time so long that people using autofill fail. |
| Count per network address | Slows down many requests from the same source. | Bots with many addresses. Company networks often share one address. | Counting every failed attempt, so that typos lead to a block. |
The W3C calls hidden fields an approach that is easy to implement, even in hand-written HTML (W3C note, Section 2.2.4 (external site)). For the origin comparison, OWASP’s guidance on cross-site requests (external site) adds that a missing header is no proof of a request from your own website.
Signed means the server uses a secret key to compute a checksum (HMAC) over the time and a random value. If anyone changes the value, the checksum no longer matches. OWASP describes the same technique for protection tokens that a server issues and later checks.
A static page can’t carry this timestamp, because it’s the same for every visitor and sits ready-made on the content delivery network (CDN). Without JavaScript, the program on the server therefore issues the actual submission form only after a first step. That way, the timestamp is created fresh for each request.
Done when all four checks run on the server, each with its own error case, and the hidden field is concealed from assistive technology.
How do you count requests without storing IP addresses?
Generate a check value from the IP address and a secret key that changes daily, and count only that value. The check value expires after one day. A hash without a key won’t do, because IPv4 has only about 4.3 billion addresses, which can be tried quickly.
The European Union Agency for Cybersecurity (ENISA) has calculated the effort. In 2019, an ordinary laptop could reverse a SHA-256 value of an IPv4 address in about two minutes. ENISA therefore recommends methods with a secret key, such as a message authentication code, which is the same technique as for the timestamp (ENISA, Pseudonymisation techniques, p. 28 (external site)).
What matters in practice:
- Keep the key separate. Store it as a secret variable at your host, not in the code. Derive a new key from it each day, and let the counters expire by the end of the day at the latest.
- Count the connection. With IPv6, a single connection has many addresses, often more than a whole /64 network (RFC 6177 (external site)). Count the prefix instead, or each new address gets around the limit.
- Count only just before sending. That way, a typo doesn’t use up an attempt.
- Offer a second route. Several people often sit behind one company network address. The message for too many requests therefore gives your email address.
Done when neither IP addresses nor their plain hashes are stored and the key is kept outside the code.
How do you stop your form from emailing strangers?
Never send an automatic message to the address someone types into the form. The inquiry goes only to your own mailbox. Your domain appears as the sender, and the address from the form appears only as the reply address. Line breaks from entries must never reach the email’s headers.
- No automatic confirmation. The NCSC describes how attackers enter someone else’s address into genuine forms at legitimate companies. Hundreds or even thousands of confirmation emails then bury a single important message, such as the alert about a bank transfer or an online order (NCSC weekly review, week 6/2026 (external site)). Without an automatic confirmation, your form can’t be used for this.
- Keep sender and reply address separate. If the address from the form appears as the sender of the email, delivery can easily fail. For direct email, Google requires the domain in the From header to align with the domain verified by SPF or DKIM (Google’s email sender guidelines (external site)). So use your own domain as the sender and put the person’s address in Reply-To (RFC 5322, Section 3.6.2 (external site)).
- Protect the headers. In what’s known as email injection, attackers use input fields to slip extra headers into an email, such as more recipients (CAPEC-134 (external site)). Use a library to send the email, and remove line breaks from every field that ends up in a header.
Done when the form writes to exactly one fixed address and a test with a line break in the name creates no extra header.
What do people see when a check fails?
Every rejection needs an explanation in words, and the entries stay in place. Silently discarded requests cost you real customers without your noticing. Only with the hidden field may the server pretend that everything worked, so that bots learn nothing.
| Case | What the page shows | Why |
|---|---|---|
| Required field missing or email invalid | Error list above the form with links to the fields, a message right at the field, entries stay in place | The W3C recommends listing errors before the form. |
| Submitted very quickly | The same form, filled in, with a request to send it again | Password managers and autofill complete forms in seconds. This way, a person loses nothing. |
| Too many requests | A page with an explanation and your email address, plus the entries so far to copy | According to RFC 6585, status 429 should explain what happened. |
| Sending failed | A page saying the fault lies with you, the operator, plus the entries to copy and your email address | Otherwise the inquiry would be lost without anyone noticing. |
The W3C advises listing errors at the top of the page, before the form, and, especially when the server processes the form, adding the word “error” to the page heading and title (W3C tutorial on form notifications (external site)). The 429 Too Many Requests response should include an explanation and can use Retry-After to say when a new attempt is possible (RFC 6585, Section 4 (external site)).
Done when you’ve triggered every case once and no entry was lost.
How do you test your own form?
Open your browser’s developer tools and send a test inquiry. The Network panel shows which services the page loads, and the Application panel shows the cookies. Then turn off JavaScript and send again. You’ll see for yourself whether the form loads third-party services and works without a script.
- Third-party addresses. Reload the page with the Network panel open and sort by domain. Anything other than your own domain is a third-party service.
- Cookies. Under Application (in Firefox: Storage), open the Cookies entry. A form without cookies shows nothing there, even after submission.
- Form target. Search the page source for
action. If the address points to another domain, the entries go there. - JavaScript off. Disable JavaScript in the developer tools settings and submit the form. The inquiry has to arrive.
- Errors. Submit with a required field left empty. The message has to appear as text, and the other entries stay in place.
You can run the same checks on our contact page. Step 4 sends a real inquiry there if you submit all the way, so please send only a genuine one.
Your form passes all five checks, and inquiries still don’t come? Then check delivery, search visibility, your offer, trust and the contact path: Why isn’t my website generating leads?Done when you’ve logged all five checks, on a smartphone too.
What applies in Germany and the EU?
A form without tracking still processes personal data. Describe the fields, purpose, recipients and retention period in your privacy policy, and sign a data processing agreement with your host and email provider. A lawyer or data protection advisor can tell you what this means legally; this section is only practical advice.
- Ask only for what you need. Every field should have a purpose, and optional fields should be marked as optional.
- Know your recipients. List who technically touches the inquiries: host, server function and email provider. Record where the server function runs.
- Set a retention period. Delete inquiries that do not lead to a contract after a fixed period. Ask your tax advisor what you are required to keep.
- Look at CAPTCHA services separately. If you do use one, check first what data it collects, where that data goes and whether it needs consent before submission.
How we describe our own form is in the contact form section of our privacy policy. It is an example and not a template for your situation.
Done when your privacy policy names the form’s fields, recipients and retention period and a qualified person has reviewed it.
From our work: how the datafalke.de form is built
The form on our contact page went online on November 7, 2026, first in German. It runs without JavaScript and without services from other providers. Step 1 is part of the static page. Step 2 comes from a program on our host’s servers, which also checks the inquiry on submission. We don’t publish the exact limits and names here because they would help attackers.
| Component | How we do it | Trade-off or limitation |
|---|---|---|
| Delivery | Prebuilt HTML, no cookie, no content from other providers; the Content-Security-Policy header forbids scripts | Every change means rebuilding and uploading |
| Two steps | Choose a topic on the static page, then enter contact details in the step from the server, which carries a signed timestamp | One more click than a single-step form |
| Protection against misuse | A field invisible to people, a signed timestamp, a limited number of requests per network address | No protection against bots that deliberately replicate the process |
| Counting | A check value from the IP address and a key that changes daily, which expires after one day at the latest; we don’t store the IP address itself for this purpose | Our host still keeps connection data in its logs for three days |
| Where it runs | The program runs exclusively on servers in the EU; the mailbox is at STRATO in Germany | We maintain the program ourselves |
| Confirmation | No automatic confirmation and no copy to the sender; a personal reply | Senders see only the thank-you page as confirmation of receipt |
| How people found us | An optional question, analyzed only in aggregate | Not every inquiry answers it |
If someone resends an inquiry that has already arrived, a second email normally doesn’t go out. The program takes them straight to the thank-you page. How the site as a whole is built is in the technology section of our colophon.
We aren’t publishing figures on blocked spam yet, because the form has been online for only a few days. If spam gets out of hand despite these checks, the last step we have planned is a calculation puzzle in the browser (ALTCHA). JavaScript would then run on the contact page after all, and what we say about it would have to change.
How we build websites is described on our website development page.
Frequently asked questions
Is a hidden field enough on its own to stop spam?
Often yes for simple bots, but not for bots written for your form. That’s our assessment. So combine it with a signed timestamp and a count per network address.
Does this work with WordPress too?
The server-side checks can be built there as well. Test any form plugin with the steps above, because plugins can load scripts or fonts. Whether a static site or WordPress suits you is covered in our comparison of static sites and WordPress.