Product updates, guides, and more

Stay up to date with the news and learn how to get the most out of the platform.

How I Stopped Spam Signups with a Custom Honeypot Captcha

How I Stopped Spam Signups with a Custom Honeypot Captcha

Sep 20, 2025 Security 👁️ 13 reads

Last updated: 2026-05-18

Oops!

We all make mistakes. Mine? I deployed a new version of the site and forgot to protect the /register route. Then I went AFK. By the time I checked back, over 250 fake accounts had been created by bots.

This happened while building StatusPage.me, and it was a good reminder that “small” auth endpoints still need boring, reliable protection — even on a product that serves status pages, not financial data.

The good news? I had already been working on a small invisible captcha library: a honeypot-based solution that costs zero UX friction for real users but stops most bots instantly. I finalized it, integrated it, and released it as gocaptcha.


What is a honeypot captcha?

A honeypot captcha adds a hidden input field to your form. Real users never see or fill it — it is hidden with CSS. Bots, which typically fill every visible and hidden field they can find in the HTML, fill it automatically. The server checks whether the field is populated; if it is, the request is rejected as bot traffic.

The technique is decades old, still works on the vast majority of commodity spam bots, and has essentially zero impact on legitimate users.


How do you stop spam signups with a honeypot captcha?

Add a hidden input field that real users never fill, then reject requests where that field is populated. Pair it with IP-based rate limiting so bot operators cannot simply remove the honeypot trigger and retry at scale. The combination stops automated signups at near-zero cost in user experience.


What I did

  1. Added a hidden <input> to the registration form, named nickname — something plausible enough that a bot would fill it without hesitation.
  2. On form submission, the server checks whether nickname is non-empty. If it is, the request is almost certainly a bot and is rejected.
  3. Wrapped the logic in a Gin middleware and released it as gocaptcha — any Gin-based app can plug it in with a few lines.

The results

After deploying the honeypot middleware, the effect was immediate.

Spam signups dropped from hundreds per day to zero — instantly.

Hehe

Here is a screenshot from the admin dashboard showing the captcha logs and the now-empty /register spam queue:

Captcha logs


How to implement it

1. Add the hidden input to your form

<form action="/register" method="post">
  <input
    type="text"
    name="nickname"
    style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;"
    autocomplete="off"
    tabindex="-1"
    aria-hidden="true"
  >
  <!-- visible fields here -->
</form>

Important details:

  • Use CSS to hide it, not display:none or type="hidden" — some bots ignore invisible or hidden-type fields but fill positioned-offscreen ones.
  • Use tabindex="-1" so keyboard users cannot accidentally tab into it.
  • Use autocomplete="off" so browser autofill does not populate it.

2. Validate on the server using gocaptcha

import (
  "github.com/gin-gonic/gin"
  "github.com/dragstor/gocaptcha"
)

r := gin.Default()

cap := gocaptcha.New(gocaptcha.Config{
    ShowBadge:      true,
    BadgeMessage:   "Protected by GoCaptcha",
    RateLimitTTL:   time.Minute,
    RateLimitMax:   10,               // max 10 attempts per IP per minute
    EnableStorage:  true,             // SQLite logs for auditing
    DBPath:         "./captcha.db",
    BlockThreshold: -5,               // block IP when score <= -5
    SkipPaths:      []string{"/auth/", "/oauth2/"},
    TrustProxyHeaders: true,          // respect X-Forwarded-For behind Caddy / nginx
})

r.POST("/register", func(c *gin.Context) {
    if cap.CheckRequest(c.Request) {
        // Redirect to login — bots "feel" like they succeeded, reducing retries
        c.Redirect(http.StatusSeeOther, "/user/login")
        return
    }
    // continue with real registration logic
})

gocaptcha handles the honeypot check and the rate limiter in a single call. The BlockThreshold lets you progressively penalize the same IP across multiple bot attempts.


Why honeypots beat CAPTCHAs for most bots

Most commercial CAPTCHA alternatives fall somewhere on a spectrum between user friction and bypass resistance:

SolutionUser frictionBot bypass rate (commodity bots)Implementation effortCost
HoneypotNoneLow — most commodity bots failMinutesFree
reCAPTCHA v2 (checkbox)Medium — checkbox + image puzzleLowLowFree (Google collects data)
reCAPTCHA v3 (invisible)NoneMedium — score-based, tunableLowFree (Google collects data)
hCaptchaMediumLowLowFree tier available
Cloudflare TurnstileVery lowLowLowFree tier available
Email verification onlyLow (email step)Medium — bots use temp addressesLowFree

When to use a honeypot alone: Your audience is mainstream users, you are running a Go/Gin app, and you want zero added friction. Stops 80–90 % of commodity spam bots with no libraries, no third-party requests, and no privacy implications.

When to add a secondary control: You are dealing with targeted bots that scrape your HTML and remove honeypot fields before submitting, or with services that use human CAPTCHA solvers (common for high-value targets). In that case, layer honeypots with Cloudflare Turnstile (minimal friction, GDPR-friendlier than reCAPTCHA) or rate limiting.

reCAPTCHA note: Google’s reCAPTCHA embeds a cross-site tracking script. For a product like StatusPage.me — where the status page is a trust signal for your users — embedding Google tracking is a brand mismatch. Turnstile or a pure honeypot avoids this.


Rate limiting as a second layer

A honeypot stops unsophisticated bots. A rate limiter stops bots that keep trying after the honeypot trigger changes (if you rotate field names) or that submit valid-looking data at volume.

gocaptcha has rate limiting built in (RateLimitTTL and RateLimitMax), but you can also add middleware-level rate limiting independently. Here is the pattern used in StatusPage.me for public tool endpoints:

// internal/middleware/rate_limit.go pattern
var registerLimiter = newRateLimiter(5, 1*time.Hour) // 5 signups per IP per hour

func CheckRegisterRateLimit(c *gin.Context) bool {
    if !registerLimiter.isAllowed(c.ClientIP()) {
        c.Header("Retry-After", "3600")
        c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"})
        c.Abort()
        return true
    }
    return false
}

Apply it before your registration handler:

r.POST("/register", func(c *gin.Context) {
    if middleware.CheckRegisterRateLimit(c) { return }
    if cap.CheckRequest(c.Request) {
        c.Redirect(http.StatusSeeOther, "/user/login")
        return
    }
    // registration logic
})

Five signups per IP per hour is aggressive enough to stop bots running at scale and lenient enough that legitimate users — who almost never create more than one account — never hit the limit.


Caveats and edge cases

Headless browser bots: Tools like Puppeteer and Playwright render pages in a real browser, execute JavaScript, and can be scripted to skip honeypot fields. A pure honeypot does not stop these. For registration forms at serious risk of targeted automated abuse, add Cloudflare Turnstile.

AI-assisted solvers: Some bot farms use on-demand human solvers or LLM-driven automation to bypass visual CAPTCHAs. A honeypot has no opinion on AI — if the bot is instructed to skip the hidden field, it will. The rate limiter becomes your primary defence here.

Autofill edge cases: Aggressive browser autofill can sometimes populate offscreen fields, triggering false positives. Using autocomplete="off" and tabindex="-1" on the honeypot field mitigates this for all major browsers.

Email verification is still required: A honeypot prevents fake account creation, but you still need email verification to confirm ownership of real email addresses. Bots occasionally use real but abandoned email addresses. Without verification, a real-looking signup from someone@gmail.com lands in your user table even if it was automated.


FAQ

Are honeypots enough on their own?

For most indie SaaS products and small-to-medium public forms: yes, combined with rate limiting and email verification. For high-value targets (fintech, crypto, anything bots monetize directly), layer honeypots with a privacy-respecting CAPTCHA like Cloudflare Turnstile.

Will this block real users?

No, if implemented correctly. Real users never see the hidden field (it is offscreen via CSS), never tab into it (tabindex="-1"), and browser autofill skips it (autocomplete="off"). The false-positive rate in production has been zero for StatusPage.me.

Do I need JavaScript for a honeypot?

No. The honeypot and rate limiter are both server-side. A user with JavaScript disabled triggers the same server-side validation as any other user. This is one advantage over JS-based CAPTCHA solutions, which often fail completely without JS.

Why redirect bots instead of returning a 4xx error?

Returning a 400 or 403 tells the bot operator the submission was rejected. A redirect to /user/login makes the bot “feel” like it succeeded, which can reduce retries and slow the operator’s feedback loop. It is a small but occasionally useful deterrent.

Does this work with frameworks other than Gin?

The gocaptcha library is Gin-native. The underlying principle — a hidden field + server-side check — works in any web framework. The HTML snippet and the server-side field check are framework-agnostic; only the middleware wrapper is Gin-specific.

Should I rotate the honeypot field name?

Occasionally. Determined bot operators can scrape your registration form, identify the honeypot field by its CSS properties, and instruct their bot to skip it. Rotating the field name every few weeks (or randomizing it per page load) raises the cost of targeting you specifically. For most apps, this is not necessary.


Want to try it yourself? Grab gocaptcha from GitHub: https://github.com/dragstor/gocaptcha

It’s lightweight, Gin-native, and it completely solved my bot problem.

If you also care about keeping your pages privacy-friendly, see: Privacy-First Web Analytics for Status Pages


Protected by gocaptcha


Author avatar
Nikola Stojković
Published Sep 20, 2025
Related Posts

No related posts were found

Share & Subscribe