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
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.

The good news? I had already been working on a small invisible captcha library - a honeypot-based solution that’s effortless for users but brutal on bots. I finalized it, integrated it, and released it as gocaptcha.

What I did

  • I added a simple hidden input field to the registration form. It’s named something like nickname and styled so that users never see it.
  • On form submission, the server checks whether the field is filled. If it is, the request is likely from a bot and is rejected.
  • I wrapped the logic in a clean Gin middleware and released it as gocaptcha - so any Gin-based app can plug it in with minimal effort.

Why honeypots work 🍯

Spam bots typically fill every field they find in a form. By introducing a field that legitimate users will never touch - but bots will - we can confidently detect automated signups with virtually no user friction.

It’s simple, elegant, and surprisingly effective.

The results

After deploying the honeypot middleware in my Gin app, the effect was immediate.

Spam signups dropped from hundreds to zero - instantly.

Hehe

💡 Here’s a glimpse from the admin dashboard, showing the captcha logs and the now-empty /register route:

Captcha logs

How you can use 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">
  <!-- visible fields here -->
</form>

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,                     // show small lock badge (optional)
    BadgeMessage:   "Protected by GoCaptcha", // badge text
    RateLimitTTL:   time.Minute,              // per-IP window
    RateLimitMax:   10,                       // max requests/window
    EnableStorage:  true,                     // enable SQLite logs + seeding
    DBPath:         "./captcha.db",           // defaults to ./captcha.db if empty
    BlockThreshold: -5,                       // block when score <= threshold
    // Bypass OAuth callbacks:
    SkipPaths:         []string{"/auth/", "/oauth2/"},
    TrustProxyHeaders: true, // respect X-Forwarded-For when behind a proxy (I use Caddy reverse proxy)
})

r.POST("/register", func(c *gin.Context) {
    if cap.CheckRequest(c.Request) {
		// redirect to a login page if bot detected, making them feel like they succeeded
		c.Redirect(http.StatusSeeOther, mainDomain+"/user/login")
		return
	}
  // If middleware passes, continue with registration logic
})

⚠️ A few notes

  • Honeypots are simple and highly effective, but they’re not bulletproof. For maximum protection, combine them with rate limiting, IP filters, and email verification.
  • Choose a believable name for the hidden field - something like "nickname" or "middle_name". Avoid anything obviously fake.
  • Don’t rely on JavaScript alone to hide the field. Use CSS, so non-JS bots still fall into the trap.

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.


Tags: captcha, spam, honeypot, gin

Want to see it in action? Head to the live demo:

Protected by gocaptcha

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

No related posts were found

Share & Subscribe