Skip to content

Fake signups: where they come from and how to filter them

· 7 min read · Disposable email

Fake signups come from a handful of distinct sources: scripts creating accounts in bulk, bots abusing your form to email-bomb other people, humans farming free trials or referral rewards, spammers using your product's own emails as a delivery channel, and ordinary people typing junk. Each source leaves a different fingerprint in your signup data and needs a different filter. A CAPTCHA stops some of them, an email check stops others, and two of them aren't stopped by either.

So before you add anything to your signup form, figure out which kind you actually have.

The five sources of fake signups

1. Bulk account creation by scripts

OWASP calls this OAT-019 Account Creation: "bulk account creation, and sometimes profile population, by using the application's account sign-up processes." The accounts get used later for spam, reputation damage, review manipulation and similar.

Fingerprint: bursts of signups in short windows, addresses at disposable domains or gibberish local parts at Gmail and Outlook, identical or empty profile fields, and almost no activity after signup except the one thing the script came for.

2. Subscription bombing through your form

This one surprises people. The email addresses are real and belong to real people who never heard of you. A bot submits a victim's address to hundreds of signup and newsletter forms at once, so the victim's inbox floods with confirmation emails. Sometimes it's harassment; sometimes it's cover, burying a fraud alert from their bank under a pile of welcome messages.

Fingerprint: legitimate-looking addresses at normal providers, many signups that never verify, spam complaints against your confirmation email, and sometimes the same address submitted repeatedly.

Email-quality checks are close to useless here. The addresses pass every test because they're genuine. What stops it is making your form expensive for bots to submit.

3. Humans farming your free tier

Real people, often one person with many identities, creating accounts to get another free trial, another batch of credits, or another referral payout. They solve CAPTCHAs fine because they're human. They use disposable inboxes, Gmail plus-addresses, and dot variants because those are free.

Fingerprint: addresses that normalize to the same inbox, disposable domains, accounts that hit usage caps quickly then go idle, referral chains that loop back to one person. We cover this in detail in stopping free-trial abuse and referral and promo-code abuse.

4. Spam delivered by your own emails

If any email you send includes text the user controls (their name in "Welcome, {name}", a workspace name in an invite, a message in a "share this" email), spammers can sign up with a URL or a scam pitch in that field and the victim's address as the email. Your domain then delivers their spam, with your authentication and reputation attached.

Fingerprint: name or company fields containing URLs, phone numbers, currency symbols or long sentences; signups where the email domain and the content of the name field have nothing to do with each other.

5. Junk from real people

test@test.com, asdf@asdf.com, no@thanks.com. Someone wanted to see the product and didn't want to give you an address. Not malicious, still useless.

Fingerprint: keyboard-mash local parts at domains that may not even accept mail, and no email verification ever.

Work out which one you have

Before you buy anything, query your own signups table. Five questions cover most cases:

-- 1. Burstiness: signups per 10-minute bucket over the last week
SELECT date_trunc('hour', created_at)
       + floor(extract(minute FROM created_at) / 10) * interval '10 min' AS bucket,
       count(*)
FROM users
WHERE created_at > now() - interval '7 days'
GROUP BY 1 ORDER BY 2 DESC LIMIT 20;

-- 2. Verification rate by email domain (low rate on big providers suggests bombing)
SELECT split_part(email, '@', 2) AS domain,
       count(*) AS signups,
       avg(CASE WHEN email_verified_at IS NOT NULL THEN 1 ELSE 0 END) AS verified_rate
FROM users
WHERE created_at > now() - interval '30 days'
GROUP BY 1 HAVING count(*) > 20 ORDER BY verified_rate ASC;

-- 3. User-controlled fields carrying links (spam via your emails)
SELECT id, email, name FROM users
WHERE name ~* '(https?://|www\.|\.(com|ru|xyz|top)\b)'
ORDER BY created_at DESC LIMIT 50;

Add two more by hand: how many signups share a normalized inbox (dots and plus tags removed), and how many land on disposable domains. A sudden cluster of unverified signups at Gmail and Yahoo with clean-looking addresses points to source 2. Lots of disposable domains and gibberish points to 1 or 3. Links in name fields means 4.

Match the filter to the source

Source Primary filter Secondary filter Email screening helps?
Bulk account creation Bot challenge, per-IP rate limit Reject disposable and randomized emails Yes
Subscription bombing Bot challenge, per-IP and per-email rate limit Don't send marketing until verified No
Free-tier farming Normalized-identity dedupe, usage caps Card for expensive features Yes
Spam via your emails Never echo user text into emails before verification Strip or reject URLs in name fields Partly
Junk from real people Email verification Domain must accept mail Somewhat

Two rules from that table are worth adopting even if you don't have a problem yet. First, don't put user-supplied text into any email sent to an unverified address. "Confirm your email" doesn't need their name in it. Second, don't send anything beyond the verification email until the address is verified. Both cost you nothing and remove the reason to target your form for sources 2 and 4.

A signup handler that covers the common cases

This TypeScript handler combines a bot challenge (Cloudflare Turnstile here; hCaptcha and reCAPTCHA work the same way) with email screening. Turnstile tokens are validated server-side against https://challenges.cloudflare.com/turnstile/v0/siteverify. Per the Turnstile server-side validation docs, each token is single-use and valid for 300 seconds.

async function verifyTurnstile(token: string, ip: string): Promise<boolean> {
  const form = new FormData();
  form.append('secret', process.env.TURNSTILE_SECRET!);
  form.append('response', token);
  form.append('remoteip', ip);
  const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
    method: 'POST',
    body: form,
  });
  const outcome = await res.json();
  return outcome.success === true;
}

async function screenEmail(email: string) {
  const res = await fetch('https://pingutil.com/v1/lookup', {
    method: 'POST',
    headers: {
      authorization: `Bearer ${process.env.PINGUTIL_KEY}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify({ input: email, skip_enrichment: true, check_dns: true }),
  });
  if (!res.ok) return null; // fail open
  return res.json();
}

const URLISH = /(https?:\/\/|www\.|\.[a-z]{2,}\/)/i;

export async function handleSignup(input: {
  email: string;
  name: string;
  turnstileToken: string;
  ip: string;
}) {
  if (!(await verifyTurnstile(input.turnstileToken, input.ip))) {
    return { ok: false, error: 'Please complete the challenge and try again.' };
  }

  if (URLISH.test(input.name) || input.name.length > 80) {
    return { ok: false, error: 'Please enter just your name.' };
  }

  const check = await screenEmail(input.email);
  if (check?.verdict.is_spam) {
    return { ok: false, error: 'Please use a permanent email address you can access.' };
  }
  if (check?.domain_signals && !check.domain_signals.can_receive_email) {
    return { ok: false, error: 'That domain does not accept email. Check for a typo?' };
  }

  // Create the user unverified. Send only a plain verification email,
  // with no user-supplied text in it. Everything else waits for verification.
  return { ok: true, lowTrust: check?.verdict.low_trust ?? false };
}

What each piece covers: the challenge handles sources 1 and 2, the name check handles 4, verdict.is_spam handles disposable and machine-generated addresses (1 and 3), and domain_signals.can_receive_email catches the asdf@asdf-nothing.com cases from source 5. The domain_signals fields come from DNS-over-HTTPS and are informational, so the policy of rejecting non-receiving domains is yours to decide. If you'd rather turn these into one number than a series of if statements, see building a simple email risk score from free signals.

Add a per-IP rate limit in front of all of this at your edge or framework level. A handful of signups per IP per hour is generous for real traffic. Also rate-limit per email address, so the same victim can't be submitted fifty times.

What none of this catches

Be honest with yourself about the gaps:

  • A determined human with a real Gmail account, a real name and a residential IP looks like a customer, because for signup purposes they are one. Only behavior after signup (usage patterns, payment, links between accounts) separates them.
  • Disposable domain lists lag behind new domains. Why disposable email blocklists go stale explains why and what to do about it.
  • Randomness heuristics misfire on some real names and on privacy relays. That's why the handler above doesn't add a separate hard block on randomness scores beyond the verdict.
  • Bot challenges get solved by challenge-solving services. They raise cost; they don't make abuse impossible.

Start here

  1. Run the diagnostic queries against your last 30 days of signups.
  2. Stop echoing user text into emails sent before verification.
  3. Put a bot challenge and per-IP plus per-email rate limits on the form.
  4. Screen emails for disposable domains and machine-generated local parts. You can see what that returns for any address in the live demo.
  5. Delete or suppress accounts that never verify within a week, and stop mailing them.

Questions people ask

Why am I getting fake signups with real people's email addresses?
Usually because a bot is using your signup form to send your confirmation email to victims, a pattern called subscription or list bombing. The addresses are real, so email checks will not catch it. Put a bot challenge and per-IP rate limits in front of the form.
Will a CAPTCHA stop all fake signups?
No. A CAPTCHA raises the cost of scripted signups, but it does nothing about humans creating throwaway accounts for free trials or referral bonuses. Pair it with email screening and limits on what a new account can do.
Should I delete fake accounts or leave them?
Delete or suppress accounts that never verified their email after a set period, and stop sending them mail. Unverified accounts inflate your metrics and every bounce or spam complaint from them hurts your sender reputation.