300 Fake Signups a Day, and the CAPTCHA Was Losing
Bots do not solve CAPTCHAs anymore. People do. What it takes to defend a signup flow when the attacker’s economics beat yours.
300 Fake Signups a Day, and the CAPTCHA Was Losing
Photo by Ryul Davidson on Unsplash
Bots do not solve CAPTCHAs anymore. People do. What it takes to defend a signup flow when the attacker’s economics beat yours.
We caught it on day one, because the pattern was impossible to miss: accounts with gibberish names, and the same gibberish repeated in the part of the email address before the @ symbol, arriving by the hundreds. If you have ever filled a test database with a fake-data library, you know exactly what those names looked like. Someone’s script was creating identities the same way developers create test fixtures.
It was almost flattering. Almost. And here is the detail that made it interesting: we already had reCAPTCHA on the signup form. Every one of those fake accounts arrived with a valid, solved CAPTCHA attached.
Why anyone wants a few hundred fake accounts a day
Fake accounts are never the goal. They are infrastructure. A pile of aged, registered accounts is useful for:
- Abusing free products, trials, and promo credits at scale, rate limits
- Skewing your product metrics: conversion rates, funnel numbers, and every UX decision made on top of them
- Staging future fraud from accounts that look established, not freshly made
- Spam and phishing that borrows your platform’s good reputation
- Resale on account marketplaces, where age raises the price
- Sleeping accounts that rest quietly for years before activation are very hard to connect to an attack
That last one turned out to matter more than we knew. More on it below.
Why the CAPTCHA was losing
We were not losing to clever computer vision. We were losing to cheap human labor: CAPTCHA-solving farms, where real people solve challenges for a few dollars per thousand. The attacker’s script sends our challenge to a queue, a human somewhere solves it in seconds, and the signup continues with a perfectly legitimate solution.
Do the math the attacker did. If a solved CAPTCHA costs a fraction of a cent, and a usable fake account is worth more, then the CAPTCHA is not a wall. It is a toll booth. And if the toll is cheaper than the prize, the road stays busy.
There was a second problem with leaning on the CAPTCHA: it was not free for us either. Our monthly reCAPTCHA quota, the ceiling we had never come close to touching in normal times, was burned through in a single week. The attack was not just creating fake users. It was running up our bill to do it.
Lesson one: a CAPTCHA raises the attacker’s cost. It does not raise it above their profit. Only the whole defense does that.
The arms race
Round one: block the email domains. This turned into a chase. The first waves came from a small set of domains, always with that gibberish in front of the @. We blocked them. The attacker moved to temporary, disposable mail domains. We blocked those too. Then they moved to the big public email providers, the ones your own mother uses, and there the chase ended. You cannot block those, because half of your real customers live there. Dead end, and the attacker knew it before we did.
A quiet win along the way: confirm every email. We made sure every signup path required the user to click a link sent to the address they registered with. This sounds basic, but it closes a real hole: an attacker can no longer register made-up addresses in bulk today and plan to create the mailboxes later. No clicked link, no account. It will not stop a determined operator with real inboxes, but it raises the floor, and raising the floor is most of the job.
Round two: rate limiting. We already had two layers of throttling: a WAF at the edge and rate limiting inside the application. Both worked exactly as configured. Both were useless, because the attacker rotated through a pool of a few thousand IP addresses. Spread a few hundred signups across a few thousand IPs and no address ever crosses a threshold. Per-IP limits assume the attacker has fewer IPs than you have patience. This one had far more.
Round three: honeypot fields. We added invisible ghost fields to the signup form. Real users cannot see them; naive bots fill them in. Effect: nothing. Between human solvers in the loop and well-written automation, everyone stepped over our tripwire politely, like they had read the code. Honeypots are cheap, so keep them. Just know they catch the bots you were going to catch anyway.
Round four: change the challenge. We replaced reCAPTCHA with Cloudflare Turnstile. Three reasons. First, Turnstile does not make users solve picture puzzles, and those puzzles were exactly what the solver farms were being paid to click through, so we stopped paying for a test the attacker had already beaten. Second, it has no monthly ceiling to burn through, so the attacker could no longer run up our bill just by showing up. Third, it plugs into the rest of Cloudflare’s scoring, so a single signup decision could draw on more than one source of doubt. A challenge nobody can solve for a dollar is worth more than a puzzle anybody can.
The signals that actually worked
No spike alert ever fired, because there were no spikes. The signups flowed all day and all night, 200 to 300 per day, steady as a metronome. That flat line was itself a confession. Real signups follow daylight, weekdays, marketing pushes, and payday. Traffic with no rhythm has no humans behind it. People sleep. Scripts do not. The accounts also shared a behavioral fingerprint that no rate limiter would ever see:
- Names and emails from the same generator, recognizable at a glance
- Exactly three logins per account, and then silence forever
- No phone number, no real name, no filled-in profile, nothing a real person eventually does
One signal alone is weak, and would flag plenty of real customers. Put them together and the fake cohort separates from the real one with almost no false positives. A single thread proves nothing. A rope of them holds.
The result
No single measure won this. The stack did. Turnstile with no ceiling to burn, a confirmed email on every signup, the behavioral signals, and the IP intelligence, all pulling at once. Once they were in place together, the fake signups did not just drop. They went to zero, and stayed there. The metronome finally stopped ticking. A wall is only as good as its shortest section, so we stopped looking for one perfect brick and built the whole wall instead.
The counterattack: turn the attack into a map
Here is the part I want more defenders to hear, because it is the part that pays for the whole headache. That pool of rotating IP addresses was not only an obstacle. It was a map. We collected it, then checked it against our history, and it lit up sleeper accounts created years before this incident, quietly aging for a rainy day that never came.
An attacker reuses infrastructure across time, because infrastructure costs money and old habits are cheap. So every attack you survive hands you intelligence about the attacks you have not seen yet. Keep it, index it, and run it against your past as well as your present. The thief left footprints on the way in. Follow them back.
Takeaways
- CAPTCHA farms turned CAPTCHAs into a toll, not a wall. Price your defenses against the attacker’s economics, not their technology.
- Per-IP rate limiting dies against a rotating pool. Assume thousands of IPs, not dozens.
- You cannot block the email providers your customers use. Identity and behavior signals must carry the weight that domain blocking cannot.
- Require a confirmed email on every signup path. It stops attackers from stockpiling made-up addresses to activate later.
- Watch your defenses’ own costs. Our never-touched CAPTCHA quota was gone in a week, so we moved to a challenge with no ceiling to burn.
- A perfectly flat signup rate is as suspicious as a spike. Real humans have a rhythm.
- Honeypot fields do not beat human-in-the-loop attacks. Keep them, but do not lean on them.
- The attack is also intelligence. A collected IP pool found us sleeper accounts planted years earlier.
I am a software engineer who occasionally has to deal with fraud, abuse and application security. I stop bots, account takeovers, and the attacks that come from inside. More at msuliq.github.io.
메타데이터
- post_id
- 5d6d3d8a4802
- slug
- 300-fake-signups-a-day-and-the-captcha-was-losing-5d6d3d8a4802
- url
- https://medium.com/@msuliq/300-fake-signups-a-day-and-the-captcha-was-losing-5d6d3d8a4802
- canonical_url
- https://medium.com/@msuliq/300-fake-signups-a-day-and-the-captcha-was-losing-5d6d3d8a4802
- author_url
- https://medium.com/@msuliq
- status
- ok
- fetched_at
- 2026-09-16 02:30:26