Docket

SELF-HOSTING

Spam on a public feedback form

A ladder of defences ordered by how little they cost the honest person filling in the form.

7 MIN READ Last updated 23 August 2026

A public feedback form with no login is an open door, and the usual fix, a CAPTCHA, punishes every honest visitor to stop a minority of bots. The better approach is a ladder of defences ordered by cost to a real person: a hidden field, then timing, then rate limits, then confirmation, then a human look, and a challenge only as the last resort.

Why does a public form attract spam at all?

Any form on the open internet that submits without an account gets found by automated scanners within days, testing huge lists of URLs for anything that accepts a POST request, not your product specifically. A self-hosted feedback board is exactly this shape, since raising the barrier to submit a feature request defeats the point of the board.

A board that only lets logged-in users post has already solved spam, by making everyone create an account first. That is its own cost. The ladder below is for boards that want to stay open and stay clean.

What actually stops spam, in order of cost to the honest user?

Every defence below costs the genuine visitor something, even if nothing they notice. The cheap ones run on every submission; the expensive ones only run when the cheap ones were not enough.

DefenceWhat it stopsCost to the honest user
Honeypot fieldNaive bots that fill in every field they findNone, if hidden correctly
Timing checkScripts that submit in under a second or twoNone
Rate limitingRepeated automated submissions from one sourceNone, unless they retry fast
Email confirmationFake addresses, most one-off bot postsOne click, one extra minute
Moderation queueContent that passes every automated checkA delay before the post is public
Challenge, last resortTargeted, adaptive submissionReal friction, a barrier for some disabled users

None of these makes the form spam-proof. They make spam progressively more expensive, and a determined human working against your form specifically will get past most of them.

How does a honeypot field actually work?

A honeypot is an extra field on the form that no real person should ever fill in. Bots that scrape a page's HTML and fill in every input they find will fill this one in too, and any submission arriving with it non-empty gets silently discarded, no error shown, no signal the bot failed.

The common mistake is hiding the field visually with CSS while leaving it exposed to screen readers, turning the trap into a field a blind visitor can tab into and fill, so the honest visitor is the one discarded.

<div class="hp-wrap" aria-hidden="true" style="position:absolute;
     left:-9999px; width:1px; height:1px; overflow:hidden;">
  <label for="website">Leave this field empty</label>
  <input type="text" id="website" name="website"
         tabindex="-1" autocomplete="off">
</div>

aria-hidden="true" removes it from the accessibility tree, tabindex="-1" takes it out of tab order, and positioning it off-screen, rather than display: none, keeps it technically present but unreachable, sighted or not. On the server, reject any submission where the field is non-empty.

A honeypot stops naive bots and nothing else. It is the cheapest defence here, which is why it belongs first, not why it belongs alone.

How does a timing check catch what a honeypot misses?

Record when the form was rendered, either in a hidden timestamp field or a session token, and compare it to when the submission arrives. A real person typing a sentence or two takes several seconds; a script that fetches and posts the form in one request typically completes in well under one.

Reject submissions faster than a threshold you set, two to three seconds is a reasonable floor. This catches a bot the honeypot misses: one that never fills in extra fields but still submits instantly, because it never rendered anything, it just replayed the form's field names.

What does rate limiting actually prevent?

Rate limiting caps how many submissions can come from one source in a window, by IP address and, where you can compute one, by a browser fingerprint. Neither signal alone is reliable, a single IP can represent many people and a sender can rotate IPs freely, so both together catch more.

This does not stop one well-crafted submission. It stops the pattern that makes spam worthwhile: the same content sent dozens of times, hoping a fraction survives moderation. Cap submissions per IP per hour generously enough that a team member testing the form never notices it.

Why require email confirmation before a post appears?

Everything so far runs silently. Email confirmation is the first defence with a visible cost: the person opens a message and clicks a link before their post appears publicly.

What it buys is real: it filters out the fake or throwaway addresses most automated spam uses, since running a confirmation loop against every invented address is expensive for a sender.

Keep the unconfirmed post out of public view but not discarded, so someone who gets distracted before clicking the link can come back and confirm later rather than resubmit.

Your own support centre, in your own repository

One payment, no subscription, unlimited products.

Why queue first-time posters for a human look?

A moderation queue for first-time posters is the point where an actual person reviews content before it goes live. It is more expensive, because it needs a human to look, but it catches what nothing automated can: content that is well-formed, arrives at a normal pace, and is still spam.

Once someone has posted once and been approved, later posts from that confirmed identity can skip the queue.

Should you ever add a challenge, and what does it cost?

A CAPTCHA or similar challenge is the last rung, not the first. CAPTCHAs are a genuine barrier for disabled users: some visual challenges are effectively unsolvable for blind visitors even with an audio alternative offered, and image-selection challenges assume vision and motor control not every visitor has. A feedback form exists to hear from your users, and a barrier that filters some of them out by disability is a real cost.

the very nature of the interactive task inherently excludes many people with disabilities, resulting in a denial of service to these users

W3C, Inaccessibility of CAPTCHA

Reach for it only after the cheaper layers above have absorbed most of the load, as a response to a form under sustained attack rather than a default switched on everywhere. It stops naive automation completely and targeted abuse only partially, since challenge-solving services exist to defeat it for anyone willing to pay per solve.

What if all of this still lets something through?

It will, occasionally, and that is the honest expectation to set rather than a failure of the setup. None of these measures makes the form spam-proof, only progressively more expensive to abuse.

Build in a simple way to remove a post after the fact, and treat an occasional spam post as the acceptable cost of keeping the form open to strangers. A form airtight against spam because it is also airtight against strangers has just moved the cost onto the people the board exists to hear from.

For the setup this all sits inside, see self-hosting a feature request board. For what to do once feedback starts arriving, see how to collect feedback from customers.

Frequently asked questions

Does a honeypot field stop every bot?

No. It stops bots that fill in every field they find without checking whether it is visible. A bot written specifically against your form, by someone who has looked at its markup, will simply skip the field it recognises as a trap.

Is a CAPTCHA ever the right choice for a public feedback form?

As a last resort, on a form under sustained attack the cheaper defences are not absorbing. It is still a genuine barrier for some disabled visitors, so that cost should not be paid by default.

How strict should the timing check threshold be?

A floor of two to three seconds is generally safe: no honest visitor writing even a short message will trip it, while a script posting in the same request as the page fetch completes far faster.

Should rate limiting block by IP address or by browser fingerprint?

Both together, rather than either alone. IP addresses can represent many genuine people behind shared infrastructure, and a sender can rotate them freely, so a fingerprint signal catches what IP-based limiting misses, and the reverse is also true.

Why does email confirmation help if an attacker can use a real address?

It raises the cost of sending spam at volume rather than making one submission impossible. Running a confirmation loop against every invented address is expensive enough that most automated spam does not bother.