
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.

💡 Here’s a glimpse from the admin dashboard, showing the captcha logs and the now-empty /register
route:
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: