Skip to content
← All articles
Security · Updated 2026-10-07

Learn · Security

Honeypot fields done right

A honeypot is an input no human should fill. Generic form-spam scripts parse the HTML and put a value in every field they find, because filling everything is the cheapest way to pass required-field checks. If the trap field has a value, the submission came from a script.

It costs nothing and adds no friction. It also produces false positives when done carelessly, mostly through autofill and assistive technology. Here is a version that avoids both.

The markup

html

<div class="hp" inert aria-hidden="true">
  <label for="hp-field">Leave this field empty</label>
  <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off">
</div>

<style>
  .hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
</style>

Each part is there for a reason:

  • type="text", not type="hidden". A script that fills visible-looking inputs skips hidden ones by definition, so a hidden input catches nothing.
  • Moved off-screen with CSS rather than removed. It stays in the DOM and in the submitted data, which is what makes it a trap.
  • inert on the wrapper takes the field out of the tab order and the accessibility tree, and stops users from editing it. It does not stop scripts setting .value, and inert fields are still submitted (only disabled ones are not).
  • tabindex="-1" and aria-hidden cover browsers that predate inert (Baseline since April 2023). The visible label text is a last resort for anyone who still reaches it.
  • autocomplete="off" asks autofill to leave it alone. Browsers treat it as a hint, not a rule, which is why the name matters too.

Naming: tempting to bots, invisible to autofill

Autofill decides what to put in a field from its name, id, label text and autocomplete value. The autofill vocabulary includes name, email, tel, organization, street-address and url, among others. Name a honeypot email2, company or website and some real visitors will have it filled by their browser or password manager, then get silently filed as spam.

Pick a name that matches no autofill category and no label a browser would pattern-match. A neutral token like _gotcha or hp_field works. Bots that fill every input do not need the name to be tempting; bots that do read names are the targeted kind a honeypot will not stop anyway.

The server check

Treat any non-whitespace value as a hit. Answer with exactly the same response a real submission gets, and keep the submission flagged as spam instead of dropping it, so you can audit false positives:

js

app.post("/contact", express.urlencoded({ extended: false }), async (req, res) => {
  if (String(req.body._gotcha ?? "").trim() !== "") {
    await saveSubmission(req.body, { spam: true });  // keep it for review
    return res.redirect(303, "/contact/thanks");      // same as the real path
  }
  await saveSubmission(req.body, { spam: false });
  await notifyOwner(req.body);
  res.redirect(303, "/contact/thanks");
});

If the success path sends JSON for fetch() clients, the spam path must send the same JSON. Any difference, including a missing id or a faster response, is a signal someone can learn from.

What a honeypot will not catch

Headless browsers that render the page can check visibility and skip off-screen inputs. Humans paid to submit forms see what you see. Floods from one address pass the honeypot as long as the field stays empty. Pair it with rate limiting and the other layers.

Test both directions

Fill the trap in devtools and submit: you should see the normal thank-you response and the submission marked as spam. Then submit with your browser's autofill and a password manager: the trap must stay empty. Repeat with a screen reader if you can; the field should never be announced.

How this works with Formward

Formward's honeypot field is _gotcha. Any non-blank value stores the submission as spam, returns the same success response as a real submission (redirect for classic posts, { ok: true } JSON for fetch) and does not count toward your monthly quota. You can review and release spam from the dashboard.

The name is fixed, so use the markup above with name="_gotcha". See the spam filtering docs for the other layers that run alongside it.

Sources

Point a form at an EU endpoint

Formward receives the POST, filters spam and stores submissions in Sweden. No server to run.

Honeypot fields for forms, done right | Formward