Skip to main content

Aug 5, 2026 · 5 min read

What Is a Honeypot Field (and Why It Stops Spam)?

A honeypot field is a decoy input hidden from people but left for simple bots to fill. See accessible markup, server checks, limits, and when CAPTCHA helps.

A honeypot field is a decoy form input that people should leave empty but simple bots often fill. Your server checks the field before saving the submission. A non-empty value is strong evidence of automation, so the server can silently discard the request without making a visitor solve a CAPTCHA.

I use honeypots as the first spam layer in Form Plume because they are cheap, invisible, and effective against unsophisticated form fillers. They are not proof that every empty submission came from a person. The layered contact form spam guide covers what should sit behind the trap.

For Form Plume setup and current defaults, use the spam protection reference. For more implementation guides, browse the Form Plume blog.

A honeypot works by giving bots one extra field

Many basic bots scan a page, find its inputs, and put a value in each one. A real visitor never sees the decoy field, so a normal submission leaves it blank.

The receiver applies a simple rule:

honeypot is empty     -> continue normal validation
honeypot has a value  -> return normal-looking success and discard

The check belongs on the server. A browser-only condition can be removed, skipped, or bypassed by posting directly to the endpoint.

OWASP includes hidden form fields among its honeypot controls, but also recommends layered defenses because any single control is brittle. That distinction matters: a honeypot catches bots that behave naively. It does not certify that everyone else is human.

Use a normal text input, not type="hidden"

This is the markup I recommend for a plain HTML contact form:

<form action="https://api.formplume.com/f/{public_slug}" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>
 
  <div class="form-honeypot" inert>
    <label for="company-website">Leave this field empty</label>
    <input
      id="company-website"
      name="_gotcha"
      type="text"
      autocomplete="off"
    >
  </div>
 
  <button type="submit">Send</button>
</form>
.form-honeypot {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

Replace {public_slug} with your form's public slug. If you use another backend, keep the same pattern and change the endpoint and field name to match its contract.

Do not make the decoy <input type="hidden">. Hidden inputs are ordinary control fields, and a bot can trivially skip them by type. A text input moved off-screen still looks like a field to a basic form filler.

The inert wrapper keeps the decoy and its descendants out of the tab order and accessibility tree while the CSS moves it off-screen. MDN documents inert as disabling focus and assistive-technology exposure.

autocomplete="off" asks browsers and password managers not to fill the trap. MDN documents that off is only a hint, so test the deployed form with the autofill and password managers your audience actually uses. A decoy accidentally filled for a real person becomes a false positive.

Check the honeypot before storage or side effects

Here is a minimal Node 22 demo for exercising both honeypot paths locally. It uses only built-in APIs and writes accepted submissions to a local JSON Lines file. It intentionally omits production controls such as request-size limits, content-type checks, durable storage, and centralized error handling.

// server.mjs
import { appendFile } from "node:fs/promises";
import { createServer } from "node:http";
 
const port = Number(process.env.PORT ?? 3000);
 
createServer(async (request, response) => {
  if (request.method !== "POST" || request.url !== "/contact") {
    response.writeHead(404).end();
    return;
  }
 
  const chunks = [];
  for await (const chunk of request) chunks.push(chunk);
  const form = new URLSearchParams(Buffer.concat(chunks).toString("utf8"));
  const honeypotFilled = (form.get("_gotcha") ?? "").trim() !== "";
 
  if (!honeypotFilled) {
    form.delete("_gotcha");
    await appendFile("submissions.jsonl", `${JSON.stringify(Object.fromEntries(form))}\n`);
  }
 
  response.writeHead(200, { "content-type": "application/json" });
  response.end(JSON.stringify({ ok: true }));
}).listen(port, () => console.log(`Listening on http://localhost:${port}`));

Run it with node server.mjs, then exercise both paths from another terminal:

rm -f submissions.jsonl
curl -i -d 'email=person@example.com&_gotcha=' http://localhost:3000/contact
curl -i -d 'email=bot@example.com&_gotcha=filled' http://localhost:3000/contact
wc -l submissions.jsonl

Both requests should return 200 with the same {"ok":true} body. The final command should report one line, because only the empty-honeypot request was stored.

Run this check before writing to the database, sending email, uploading files, firing a webhook, or incrementing usage. Otherwise the bot still consumes the expensive work you meant to avoid.

Return a normal-looking success response. A descriptive honeypot_failed error tells the bot exactly what to change on its next attempt. The decoy response does not need to create a real submission ID, but it should follow the same public response shape closely enough that the trap is not obvious.

Do not log the submitted honeypot value unless you have a specific diagnostic need. The value is untrusted input, and storing it adds data without improving the decision.

Avoid false positives from autofill and accessibility mistakes

MistakeWhat happensBetter choice
The field stays interactiveA keyboard or assistive-technology user can reach an unexplained inputMake the wrapper inert
Autofill populates the decoyA legitimate submission looks automatedUse autocomplete="off" and test real autofill tools
CSS fails to loadThe trap becomes visibleKeep a clear “Leave this field empty” label as a safe fallback
JavaScript alone enforces the ruleDirect POST requests bypass itCheck again on the server
The server returns a bot-specific errorAttackers learn the detection ruleReturn a normal-looking success

Off-screen positioning keeps a conventional text control in the document for simple parsers. It is still only an implementation choice, not a security boundary. A capable browser bot can inspect either technique.

Honeypots stop simple bots, not every spammer

The trap will still miss:

  • Bots configured to ignore known names such as _gotcha
  • Headless browsers that inspect styles, labels, or visibility
  • Scripts that post directly with only expected field names
  • Human spammers
  • Automation that learns from your response behavior

If you control the receiver, or your backend explicitly supports a custom honeypot name, changing the field name can catch bots hard-coded to skip a popular convention. Form Plume currently requires _gotcha. Obscurity only buys time, so do not make the name a secret your spam system depends on.

When the trap stops enough noise, leave it alone. When abuse gets through, add a signed time-trap token, server-side rate limits, and content scoring. Use CAPTCHA as a later step-up for persistent browser automation, not as the automatic replacement for a lightweight field.

Form Plume discards filled _gotcha requests before side effects

Form Plume reserves _gotcha as a special form field and recognizes it as its honeypot field. If _gotcha is non-empty, JSON clients receive a normal-shaped 200 response with a decoy ID. Native HTML submissions receive a 303 to a request-level _redirect or _next, or to /thanks/{slug} when neither is supplied. Because this branch runs before the form lookup, it does not use a success redirect saved in the form's settings. No submission is created.

The handler then exits before creating a submission, storing direct-upload objects, linking presigned files, metering usage, or enqueueing notification and webhook processing.

That behavior is deliberate. A real visitor gets no challenge, and the bot does not receive a useful rejection signal. The honeypot setup reference keeps the current product contract in one place.

I still pair it with the time trap and rate limits. The honeypot catches careless bots. The other layers can catch some clients smart enough to leave the field alone.

Before publishing your form:

  1. Add the decoy text input and keep it out of sight, keyboard order, and assistive technology.
  2. Submit once with the field empty and confirm the real entry is stored.
  3. Submit once with the field filled. Confirm you receive the normal success response, but no dashboard entry, notification, or webhook appears. If the form accepts files, confirm no attachment appears because no submission was created.
  4. Test autofill and a password manager, then add timing and rate controls if spam still gets through.

Danilo Vilhena

One line. Zero backend.

The form backend you don’t have to build.

500 submissions/month freeNo credit cardPro from $9/month