invisible_captcha: a honeypot Rails field that only bots fill in
🍯 Unobtrusive and flexible spam protection for Rails apps
At a glance
- What is it?
- Invisible Captcha is a Ruby gem that protects Rails forms with three signals: a hidden honeypot field, a minimum submission time, and an IP-based spinner check, with the default spam response being an empty 200 so the bot believes it succeeded. The interesting constraints are the ones about multiple app instances and browser autofill.
- Who is it for?
- invisible_captcha fits a Rails application whose public forms attract form spam and whose owners would rather not ask every real user to solve a challenge, and it fits applications that already render their forms server side. It does not fit a JavaScript-rendered form, because the gem relies on data set by the backend, and it does not fit a multi-instance deployment that has not been configured to share its honeypot collection and secret.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 102 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One field that humans cannot see and bots cannot resist
Invisible Captcha is described as a complete and flexible spam protection solution for Rails applications, and the repository summary calls it unobtrusive and flexible spam protection for Rails apps.
The main protection is based on the honeypot principle, which the README says provides a better user experience since there are no extra steps for real users, only for the bots. The strategy is to add an input field into the form that shouldn't be visible to real users, should be left empty by real users, and will most likely be filled by spam bots.
That is the whole idea, and it is a good one. A bot that parses forms and submits every field it finds will fill the hidden input. A person looking at a rendered page will not, because the field is not there to be filled. There is no puzzle, no image, no friction, and nothing for the user to notice.
The gem is a Ruby package published on RubyGems as invisible_captcha, MIT licensed, tested against Rails >= 5.2 and Ruby >= 2.7, and the repository shows a conventional gem layout with a gemspec, a Rakefile, a spec directory, an Appraisals file and a gemfiles directory, which is the standard way to test a gem against several dependency versions.
The audience is a Rails developer with a public form, a comment endpoint or a contact form, who is receiving submissions from scripts and has decided that a visible challenge is a worse trade than a hidden field.
The default answer to a spam submission is an empty 200
The controller integration is one line, and the behaviour it triggers is the detail that makes this gem worth reading.
class TopicsController < ApplicationController
invisible_captcha only: [:create, :update], honeypot: :subtitle
endThe invisible_captcha method acts as a before_action that triggers when spam is detected, meaning the honeypot field has some value. By default, it responds with no content, only headers, head(200).
The README explains why that is the default, and the reasoning is good. The bot will surely read the response code and will think that it has achieved to submit the form properly. A captcha that returns an error page teaches a bot that it was caught, and a bot that knows it was caught is a bot that gets smarter. An empty 200 costs the bot nothing and costs the human nothing, because the human never sees the field.
You can define your own callback by passing a method to the on_spam option, and the README's example redirects to the root path instead. One warning comes with it: you should not name your method on_spam, as this will collide with an internal method of the same name.
The spam message itself goes to flash[:error], which means the layout has to render it. The README shows the full layout snippet, with <%= flash[:error] %> placed near the top of the body element, and notes that you can place it next to your :alert and :notice message types if you have them.
Two more signals ride along: submission time and an IP spinner
The honeypot is the main protection, and the gem adds two more techniques behind it.
The first is a time-sensitive form submission, described in the README as an hourglass check. The timestamp_threshold option is the fastest time, in seconds, to expect a human to submit the form, and it defaults to 4 seconds. The README credits the idea to the original article by Yoav Aner outlining the idea, and that article is about simple detection of comment spam in Rails.
The second is an IP based spinner validation, described in the list of what comes with the gem. The README does not explain the mechanism in detail, so it is best treated as a component whose behaviour you learn by running it rather than by reading the documentation.
Both can be switched off independently. timestamp_enabled is the option to disable the time threshold check at application level, and the README suggests it could be useful, for example, in some testing scenarios; it defaults to true. spinner_enabled is the option to disable the IP spinner validation, also defaulting to true, and honeypot_enabled is the option to disable the honeypot field at application level, also defaulting to true.
That last combination is the interesting part of the design. A deployment can run the honeypot alone, the timestamp alone, or the spinner alone, and the initializer exposes all three. The trade is that each signal removes a class of bot and adds a class of false positive, so turning one off is a decision about which mistakes you would rather make.
The timestamp check is the one most likely to catch a human, and the README is direct about it: it is recommended to deactivate the autocomplete feature to avoid false positives, with autocomplete="off".
Everything is configurable from one initializer
The options are set in a Rails initializer, and the README recommends config/initializers/invisible_captcha.rb.
InvisibleCaptcha.setup do |config|
# config.honeypots << ['more', 'fake', 'attribute', 'names']
# config.visual_honeypots = false
# config.timestamp_threshold = 2
# config.timestamp_enabled = true
# config.injectable_styles = false
# config.spinner_enabled = true
# config.honeypot_enabled = true
endThe options worth understanding are the ones that are not obvious. The sentence_for_humans option is the text shown to a real user if the input field was visible, and by default it uses I18n, so the same gem serves a localised site without configuration. The honeypots option is the collection of default honeypot field names, used by the view helper when it is called with no arguments to generate a random field name, and by default a random collection is already generated.
The visual_honeypots option makes the honeypots visible, and the README says it is also useful to test and debug your implementation, which is the fastest way to confirm that your markup really is hiding the field.
The injectable_styles option defaults to false, in which case styles are injected inline with the honeypot; when you enable it you have to call the invisible_captcha_styles helper anywhere in your layout, for example in the head, to get the same effect. And the secret option customises the secret key used to encode some internal values; by default it reads the environment variable ENV['INVISIBLE_CAPTCHA_SECRET'] and falls back to a random value.
Multiple Rails instances need the same honeypots and the same secret
This is the constraint that catches people, and the README has a dedicated section for it.
Because the random collection of honeypot field names is stored in memory, it will not work if you are running multiple Rails instances behind a load balancer. The random name is generated on one instance and the form is submitted to another, which does not recognise the field, and the result is a stream of submissions that are not caught.
The fix is to share the collection between the instances, either by using a fixed collection or by sharing them between the instances using Rails.cache. The README's snippet starts exactly that way, with config.honeypots = Rails.cache.fetch(...), and the fixed collection option is the commented-out line in the initializer example, config.honeypots << ['more', 'fake', 'attribute', 'names'].
The same problem applies to the secret. The README says it directly: if you are running multiple Rails instances behind a load balancer, use always the same value via the environment variable. A per-instance random secret means that a value encoded by one instance is not decodable by another, so the same request that one instance accepts can be rejected by the next.
Both are configuration changes, not code changes, and both need to happen before the first deploy rather than after the first incident. The two hourglass and spinner signals are unaffected, because they do not depend on shared state.
Where it fails: JavaScript-rendered forms and browser autofill
Two limitations are stated clearly enough to plan around.
The first is the rendering requirement. The gem relies on data set by the backend, so in order to properly work, your forms should be rendered by Rails. Forms generated via JavaScript are not going to work well.
That is a direct consequence of the design. The honeypot field is a real input element in the server-rendered HTML, and its value travels with the form submission, along with the timestamp and whatever the spinner check attaches. A form built entirely on the client has to reconstruct all of that, and the gem's helpers are not built for it.
The second is the browser. The README warns that Chrome may ignore autocomplete="off", and suggests not using field names that would be autocompleted, like for example name, country. A browser that ignores the instruction and fills a hidden field with the user's own name is a false positive, and the standard defence is to choose a field name the browser has no reason to fill. The same advice appears for the timestamp check, where the recommendation is to deactivate the autocomplete feature to avoid false positives.
The third is the honest limitation of the whole approach, which follows from how honeypots work. A bot that renders the page with a headless browser, checks whether the field is visible before filling it, and submits quickly will pass every check the gem makes. Invisible Captcha reduces the volume of spam cheaply, and a determined spammer who looks at the form is not stopped by a honeypot. The gem is for the background noise, not for an adversary.
MIT, a gem on RubyGems, and a versioned dependency matrix
The licence is MIT, with the LICENSE file at the root of the repository, so the gem can be used in a commercial application without obligation beyond keeping the notice.
The packaging is conventional for a mature Rails gem. The repository has an invisible_captcha.gemspec, a Gemfile, a Rakefile, a .rspec file, a CHANGELOG.md and a lib and spec directory, and it has a CI workflow with a build badge and a codecov badge for coverage, which is the arrangement you would expect from a gem with a real test suite.
The Appraisals file and the gemfiles directory are the detail that speaks to the support matrix. That pattern exists to generate and test against several dependency versions, which matches the README's statement that the gem is tested against Rails >= 5.2 and Ruby >= 2.7, a deliberately old floor that lets an application still on an older Rails release adopt it.
The last push to the repository was on 2026-07-15, and the project has no GitHub releases, so the version history that matters is the one on RubyGems and in the CHANGELOG.md. The gem is published as invisible_captcha, and the installation is one line in a Gemfile followed by bundle install.
The honest overall assessment is that this is a mature, narrowly scoped gem that solves one problem well: stopping naive form spam without asking a real user to do anything. Its failure modes are known, documented and mostly configurable, which is the standard you should hold a small security library to.
Editorial conclusion
invisible_captcha fits a Rails application whose public forms attract form spam and whose owners would rather not ask every real user to solve a challenge, and it fits applications that already render their forms server side. It does not fit a JavaScript-rendered form, because the gem relies on data set by the backend, and it does not fit a multi-instance deployment that has not been configured to share its honeypot collection and secret. Before you enable it, read the honeypots and secret options in the README, because a load balanced deployment that ignores them will pass bots and reject real users at random.
Frequently asked questions
How do I add Invisible Captcha to a Rails application?
Add gem 'invisible_captcha' to your Gemfile and run bundle install, then call f.invisible_captcha :subtitle or invisible_captcha :subtitle, :topic in the form, and add invisible_captcha only: [:create, :update], honeypot: :subtitle in the controller. The gem is tested against Rails >= 5.2 and Ruby >= 2.7.
What happens when Invisible Captcha detects spam?
The invisible_captcha method works as a before_action and, by default, responds with no content, only headers, head(200), so the bot believes the submission succeeded. You can pass a method to the on_spam option to define your own callback, but you should not name that method on_spam because it collides with an internal method.
Why does Invisible Captcha need configuration on multiple Rails instances?
The random collection of honeypot names is stored in memory, so it does not work with multiple instances behind a load balancer unless the collection is shared, using a fixed collection or Rails.cache. The secret has the same constraint and should be set to the same value through the INVISIBLE_CAPTCHA_SECRET environment variable.
Can Invisible Captcha work with forms rendered by JavaScript?
Not well. The README states the gem relies on data set by the backend, so forms should be rendered by Rails, and that forms generated via JavaScript are not going to work well.
How do I debug the Invisible Captcha honeypot field?
Set the visual_honeypots option, which makes the honeypots visible and is described as useful to test and debug your implementation. The timestamp check is separate and is controlled by timestamp_threshold, which defaults to 4 seconds, and timestamp_enabled, which can be switched off for testing scenarios.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/markets-invisible-captcha)Was this useful?