All articles

reCAPTCHA Not Stopping Spam? Why People Get Through, and What Works

Published Updated By GoodInbound team

Still getting spam with reCAPTCHA? Why people and bots get past it, a server-side verification checklist with code, and the layers that stop what it misses.

reCAPTCHA asks one question: is this a human? A lot of what reaches contact forms today is sent by a human: people pitching SEO, web development, and link building, often with AI-written text. They pass any CAPTCHA, because they're exactly what it's built to let through. The bots that still get past it usually exploit gaps in the setup: a token that's never checked on the server, a v3 score nobody acts on, or a form endpoint that accepts posts with no token at all.

This guide helps you tell which problem you have, fix the setup (with verification code for Node and PHP), and add the checks a CAPTCHA can't do. For every method side by side, see how to stop contact form spam.

GoodInbound Lead Screen catches the spam reCAPTCHA lets through. Add one script to your existing form and every submission is screened, with no CAPTCHA and nothing for the visitor to do:

  • Stops bots and fake sign-ups without a puzzle, using how the form was filled, the network it came from, and the email address.
  • Catches sales pitches from people and AI, because it reads the message, not just the browser.
  • Labels every submission with a reason, and deletes nothing, so a real lead screened out by mistake is one click away.
  • Notifies you only for the labels you choose, by email, Slack, or Telegram, and sends only real leads on to your CRM and ad platforms.

The free plan screens 500 submissions a month; the message check that catches sales pitches is on Pro. Start free, or read on for every method.

First, work out which spam you're getting

Open your last 20 spam submissions and sort them into two piles. The fix depends on which pile is bigger.

  • Sent by people (reCAPTCHA can't help): full sentences, a real name and company, and an offer (SEO audits, a redesign, backlinks, lead lists). Often polished, AI-sounding copy. One or two a day.
  • Sent by bots (a working reCAPTCHA setup should catch more): gibberish, link-stuffed messages, the same text in every field, disposable addresses, and bursts of dozens in minutes.

Not sure about one? Paste it into the free form spam checker and it comes back with a label and a reason.

Why people get straight through reCAPTCHA

reCAPTCHA v2 shows a checkbox and sometimes an image puzzle. reCAPTCHA v3 shows nothing and returns a score from 0.0 (very likely a bot) to 1.0 (very likely a good interaction). Both measure the visitor, not the message.

A freelancer pasting a pitch into your form is a real person in a real browser. They tick the checkbox, solve the grid if it appears, and earn a high v3 score. AI-written copy changes nothing, because reCAPTCHA never reads the message and a person still pressed send.

So raising the threshold doesn't fix pitch spam. A stricter setting can only block more humans, and the person selling SEO is as human as the person who wants to buy from you. Stopping them takes a check that reads what was written: see how to stop sales pitches through your contact form.

Why bots get through: the technical reasons

If the bot pile is bigger, reCAPTCHA is usually installed but not enforced. These are the common gaps.

The token is never verified on the server

The reCAPTCHA widget only produces a token. Nothing is blocked until your server sends that token to Google's siteverify endpoint and acts on the answer. A form that only loads the front-end script is decoration.

The endpoint accepts posts without a token

A lot of form spam never loads your page. A script reads the form's action URL once, then posts straight to it. If your handler saves or emails the submission when the token field is missing or empty, reCAPTCHA has been skipped entirely. Treat a missing token as a failed check.

The v3 score comes back, but nothing acts on it

reCAPTCHA v3 never blocks anything by itself. It returns a score, and your code decides. Google suggests starting with a threshold of 0.5. Common slips:

  • Checking success only. success: true means the token was valid for your site, not that the visitor was human. A score of 0.1 can arrive with success: true.
  • A threshold set too low, for example dropped to 0.1 after real visitors were blocked once.
  • Not knowing the score levels. Google's docs say that without a billing account on the Google Cloud project, scores come in four levels only: 0.1, 0.3, 0.7, and 0.9. A threshold of 0.4 and one of 0.6 behave the same.

Where to change it:

  • WPForms: the Score Threshold field under WPForms → Settings → CAPTCHA.
  • Contact Form 7: the default is 0.50, changed with the wpcf7_recaptcha_threshold filter. It passes a submission only when the score is above the threshold, so with the four levels, 0.5 lets 0.7 and 0.9 through, and 0.7 lets only 0.9 through.
  • The reCAPTCHA admin console: the Security Preference slider applies to v2 keys only. v3 has no slider; the threshold lives in your code or plugin.

Scores in the first 7 days can differ from long-term ones, Google notes, so judge over a week of real traffic. More on the two versions in reCAPTCHA v2 vs v3.

You verify, but don't check action or hostname

The siteverify response says which site the token was solved on (hostname) and, for v3, which action it was created for (action). Google says to check the action matches the one you expect, and if origin verification is off in the key settings, to check the hostname yourself. Skip these and a token created for your newsletter box, your login page, or another site on the same key works on your contact form too.

One key across many sites

Agencies and developers often reuse one key on every client site. Two problems follow. A token solved on any domain listed on the key verifies for all of them unless you check hostname. And the free allowance is shared: Google gives 10,000 assessments a month free per Google Cloud organization, across all keys and sites. Past that, without billing enabled, siteverify fails open: it returns success: true with a fixed score of 0.9 and an "Over free quota." message, so every submission passes. If every score in your logs is exactly 0.9, check this first.

Google also recommends separate keys for development and production, with localhost allowed only on the development key. A production key that allows localhost accepts tokens solved on anyone's machine.

Solving services and human farms

When the setup is right, bots pay their way through. CAPTCHA-solving services take your site key and page URL, solve the challenge, and hand back a valid token. One large service lists reCAPTCHA v2 at $1 to $2.99 per 1,000 solves and says most tasks are solved by AI, with human workers as the fallback (checked October 2026). In 2024, researchers at ETH Zurich reported solving 100% of reCAPTCHA v2 image challenges with image-recognition models.

These tokens were genuinely solved for your site, so they pass every server-side check. That's the ceiling of any CAPTCHA.

WordPress: conflicts, caching, and expired tokens

On WordPress the setup tends to break quietly:

  • Two plugins load reCAPTCHA, say your form builder and a security plugin, with different keys or versions. They can conflict, and the usual fix is switching one off, sometimes the one doing the checking.
  • Script optimization that delays or combines JavaScript can stop reCAPTCHA from running before submit, so the token arrives empty.
  • Tokens generated at page load go stale. A token is valid for two minutes and can be verified once. Slow visitors get timeout-or-duplicate, and the owner lowers the bar to stop the complaints.

Each ends the same way: protection gets loosened, and the bots come back.

The debugging checklist

Work through this on the live form.

  1. Post without a token. From a terminal:

    curl -i -X POST https://example.com/contact \
      -d "name=Test" -d "email=test@example.com" -d "message=No token"
    

    If it shows up in your inbox or database, the endpoint doesn't require the token. Fix this first.

  2. Post with a fake token. Add -d "g-recaptcha-response=abc123" (or your v3 field name). This must be rejected too.

  3. Read your server code. Find the call to https://www.google.com/recaptcha/api/siteverify. If there isn't one, nothing is being verified.

  4. Log the full response for a week: success, score, action, hostname, challenge_ts, and error-codes. Compare the scores on spam with the scores on real leads.

  5. Check the key settings: the listed domains, origin verification on, no localhost on the production key, one key per site, and billing if you're near 10,000 verifications a month.

  6. Generate the token at submit, not on page load.

Correct verification posts secret, response, and optionally remoteip to siteverify, then checks every field that matters before anything is saved or emailed.

Node (18+, built-in fetch):

const EXPECTED_ACTION = "contact"; // the action passed to grecaptcha.execute()
const ALLOWED_HOSTS = ["example.com", "www.example.com"];
const MIN_SCORE = 0.5;

async function verifyRecaptcha(token, remoteIp) {
  if (!token) return { ok: false, reason: "missing token" };

  const body = new URLSearchParams({
    secret: process.env.RECAPTCHA_SECRET,
    response: token,
  });
  if (remoteIp) body.set("remoteip", remoteIp);

  const res = await fetch("https://www.google.com/recaptcha/api/siteverify", {
    method: "POST",
    body,
  });
  const data = await res.json();
  const errors = data["error-codes"] ?? [];

  if (!data.success || errors.length) return { ok: false, reason: errors.join(", ") || "failed" };
  if (!ALLOWED_HOSTS.includes(data.hostname)) return { ok: false, reason: `hostname ${data.hostname}` };
  // v3 only: v2 responses have no score or action.
  if (data.action !== EXPECTED_ACTION) return { ok: false, reason: `action ${data.action}` };
  if (typeof data.score !== "number" || data.score < MIN_SCORE) return { ok: false, reason: `score ${data.score}` };

  return { ok: true, score: data.score };
}

PHP:

function verify_recaptcha(string $token, string $action, array $hosts, float $minScore = 0.5): bool {
    if ($token === '') return false;

    $ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_POSTFIELDS => http_build_query(array_filter([
            'secret'   => getenv('RECAPTCHA_SECRET'),
            'response' => $token,
            'remoteip' => $_SERVER['REMOTE_ADDR'] ?? '',
        ])),
    ]);
    $data = json_decode((string) curl_exec($ch), true);

    return is_array($data)
        && ($data['success'] ?? false) === true
        && empty($data['error-codes'])
        && in_array($data['hostname'] ?? '', $hosts, true)
        && ($data['action'] ?? '') === $action   // v3 only
        && ($data['score'] ?? 0) >= $minScore;   // v3 only
}

// v2 posts the token as g-recaptcha-response; for v3, use your hidden field's name.
if (!verify_recaptcha($_POST['g-recaptcha-response'] ?? '', 'contact', ['example.com', 'www.example.com'])) {
    http_response_code(403);
    exit;
}

Both reject the submission when the token is missing or any check fails. Google itself rejects tokens older than two minutes or already used (timeout-or-duplicate).

If Google's docs send you to Google Cloud

They will. reCAPTCHA is now part of Google Cloud Fraud Defense, and the classic developer pages carry a deprecation notice. Google stopped issuing classic keys in 2024, and its timeline had existing keys moved into Google Cloud projects automatically by early 2026. Your siteverify code keeps working: the migration overview says SiteVerify requests continue to function as before.

For new integrations, Google recommends the CreateAssessment API (POST https://recaptchaenterprise.googleapis.com/v1/projects/PROJECT_ID/assessments) over legacy SiteVerify. The same checks apply under new names: tokenProperties.valid, tokenProperties.action, tokenProperties.hostname, and riskAnalysis.score, plus reason codes such as AUTOMATION once billing is on. One difference matters for spam: over the free quota, CreateAssessment fails closed with a 429 error instead of passing everything at 0.9.

What works: layers, then the message

A correctly verified reCAPTCHA stops a share of bots. For the rest, and the people, add layers that each catch something different, with nothing for the visitor to do.

  • Honeypot. A hidden field real visitors never see. If it's filled in, it's a bot.
  • Time trap. Record when the form was rendered (signed, or stored on the server) and reject submissions sent in under about three seconds, or with no timestamp. Bots posting straight to the endpoint fail it.
  • Rate limit. Cap submissions per IP, for example three in ten minutes, and flag datacenter IPs. Bursts stop at the door.
  • Email checks. Check the syntax, confirm the domain has a mail server, and flag disposable domains. Try a few addresses in the disposable email checker.
  • Content rules. Rejecting links in the message field catches the crudest bots. On WordPress, Akismet checks the text of comments and form submissions for spam.

Setup for each is in how to stop contact form spam without a CAPTCHA. Ready to drop reCAPTCHA? Compare reCAPTCHA alternatives and Turnstile vs reCAPTCHA.

Screen the message for human spam

None of those layers catches a polite pitch from a real person with a real address. That takes reading the message against what you sell. "Can you send pricing for 40 seats?" is a lead. "We can get you to page one of Google" is a pitch, however well written.

That's what GoodInbound's message check does. It reads the whole submission against what you sell. Sales pitches, recruiting, and guest-post offers are labeled spam; link spam, scams, and messages unrelated to your business are labeled junk. Each gets a one-line reason, such as "Selling SEO or marketing services." It's part of Pro, $29 a month for 2,500 screened submissions. The free plan runs the other checks (your rules, behavior, network, and identity) but doesn't read the message.

Where GoodInbound fits

GoodInbound Lead Screen runs these layers in one place. One script on your existing form (HubSpot, Marketo, Webflow, WordPress, Framer, or plain HTML) checks your own rules, how the form was filled, the network, the email identity, and on Pro, the message. Each submission gets a label, lead, review, spam (a person selling something), junk (fake or unusable), or bot, plus the reason.

Nothing is deleted, and a submission screened out by mistake can be rescued in one click. Only real leads go on to your CRM and ad platforms, so Google Ads, Meta, and LinkedIn count real leads as conversions. If your form posts to your own server, the HTTP API runs the same screen from your handler.

You can keep reCAPTCHA running while you try it and remove it once the labels look right. For everything else, see the full contact form spam guide.

FAQ

Why is spam still getting through my form with reCAPTCHA installed?

People sending sales pitches pass any CAPTCHA because they're human. Bots get through when the setup isn't enforced: the token isn't verified on the server, the endpoint accepts posts without one, or the v3 score is never checked.

What reCAPTCHA v3 score threshold should I use?

Google suggests starting at 0.5 and watching real traffic for a week before tightening. Without billing on the Google Cloud project, scores come in four levels only (0.1, 0.3, 0.7, and 0.9), so small moves between two levels change nothing.

Why is every reCAPTCHA score 0.9?

Most likely your Google Cloud organization has passed the free 10,000 assessments a month without billing enabled. Google's siteverify endpoint then fails open with success: true, a fixed 0.9, and an "Over free quota." message. A brand-new key can also return high scores while it has too little traffic to judge.

Does reCAPTCHA stop sales pitches?

No. A person selling SEO or development services is a human, and that's all reCAPTCHA checks. Catching pitches takes a check that reads the message, like GoodInbound's message check on Pro.

What should I use instead of reCAPTCHA?

For bots, invisible layers (honeypot, time trap, rate limits, email and network checks) or another challenge such as Cloudflare Turnstile. For pitches, screening that reads the message. reCAPTCHA alternatives compares the options.

Switch in 5 minutes

Setup with your agents