Defeating the Turing Test#

Completely Automated Public Turing test to tell Computers and Humans Apart (CAPTCHA) is the ultimate fallback for WAFs. When a system cannot determine if your TLS signature or browser fingerprint is legitimate, it serves a CAPTCHA to force human cognitive verification.

Modern scraping operations treat CAPTCHAs not as roadblocks, but as a standard operational expense. You bypass them using specialized third-party solving APIs.

The Three Eras of CAPTCHAs#

  1. Text/Image CAPTCHAs (Legacy): Distorted text or simple math problems. Easily defeated by standard OCR (Optical Character Recognition) AI models running locally.
  2. reCAPTCHA v2 / hCaptcha: "Click all squares with traffic lights." These rely heavily on tracking mouse movements and your Google session cookies.
  3. reCAPTCHA v3 / Cloudflare Turnstile: Invisible. They do not ask you to click anything. They evaluate your browser fingerprint, TLS handshake, and behavioral biometrics in the background and silently issue a pass/fail token.

How Third-Party Solvers Work#

Services like CapSolver, 2Captcha, or Anti-Captcha employ massive farms of human workers in developing nations, combined with advanced machine learning models, to solve puzzles on your behalf. The solving speed depends on stock: image CAPTCHAs solve in seconds, while full reCAPTCHA v2 flows can take 30 seconds to a few minutes.

The Token Injection Method (API-Based)#

This is the fastest, most reliable way to bypass a CAPTCHA without using a slow browser. 1. Your scraper intercepts the target URL and extracts the public site_key embedded in the page's HTML. 2. You send an API request to the solving service containing the site_key and the target URL. 3. The service (or a human worker) solves the CAPTCHA on their end. 4. The service returns a massive alphanumeric string (the response token). 5. You inject this token into your HTTP POST payload (or hidden HTML textarea) and submit the form to the target server. The server verifies the token with Google/Cloudflare and grants access.

import requests
import time

# Example of asking a service to solve a reCAPTCHA v2
API_KEY = "YOUR_SOLVER_API_KEY"
SITE_KEY = "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"
PAGE_URL = "https://target-website.com/login"

# 1. Create the task
task_payload = {
    "clientKey": API_KEY,
    "task": {
        "type": "NoCaptchaTaskProxyless",
        "websiteURL": PAGE_URL,
        "websiteKey": SITE_KEY
    }
}
task_resp = requests.post("https://api.capsolver.com/createTask", json=task_payload).json()
task_id = task_resp.get("taskId")

# 2. Poll for the result
print("Waiting for solution...")
while True:
    res = requests.post("https://api.capsolver.com/getTaskResult", json={"clientKey": API_KEY, "taskId": task_id}).json()
    if res.get("status") == "ready":
        token = res["solution"]["gRecaptchaResponse"]
        print(f"Token acquired: {token[:30]}...")
        break
    time.sleep(2)

# 3. Submit the token to the target site

Tokens Have Short Lifetimes#

Captcha tokens are ephemeral by design. A reCAPTCHA v2 token is typically valid for 120 seconds after issue, so the solve-and-inject round trip must happen within that window. Build the pipeline accordingly: extract the site key, create the solve task, and only then move the browser to the point where the token will be submitted, so it arrives fresh.

A Note on Turnstile & Invisible Schemes#

Cloudflare's Turnstile is incredibly difficult for remote workers to solve because it relies strictly on the executing browser's fingerprint. To solve Turnstile, you generally must use an excellent residential proxy paired with an immaculate undetected-chromedriver setup so the invisible test passes natively on your machine. reCAPTCHA v3 is similar: it scores your session in the background, and a low score silently throttles you without ever showing a puzzle. For those, the "solve" often happens by out-scoring rather than out-clicking—a clean session that never trips a challenge is the cheapest CAPTCHA there is.

The Taxonomy You Are Actually Facing#

"CAPTCHA" covers six unrelated families with four different economics. Treating them as one problem is why most solve pipelines waste money.

Family Signal it grades Solve surface Unit cost Failure mode when you get it wrong
Image / grid pixel classification only fully local, no egress CPU only misclassification rate, no network dependency
reCAPTCHA v2 checkbox risk score plus click vendor, proxied per task silent timeout, 120 s token
reCAPTCHA v3 / enterprise score only, no puzzle nothing to solve per assessment no token exists at all, you are scored out
hCaptcha score plus click vendor, proxied per task often two challenges per flow
Turnstile score plus managed challenge the browser itself per host solve opaque, rotates without notice
GeeTest / FunCaptcha behavioural puzzle, slider vendor, proxied per task slider trajectory is separately scored

The two invisible families (v3 and Turnstile) are not puzzles at all. There is no correct answer to return and no vendor will accept a job for them. When a v3 assessment returns a low score, the token endpoint still returns a token — a token whose embedded score tells the origin to serve you a challenge anyway. Purging that token and solving again is wasted money; the decision was made before the token existed. The only lever is the session that produced the assessment.

Token Semantics: Single-Use, Bound, and Expiring#

A challenge token is a bearer credential with three separate constraints, and vendors check all three.

  • Single use. Google, hCaptcha and Cloudflare all reject the second verification of the same token. Any retry layer that reuses the last successful token guarantees a 400 on the retry path. Your token cache must be keyed by challenge instance, not by site.
  • Binding. The token embeds the hostname, and increasingly the client IP and user agent seen at issue time. Verification happens server-side against those values.
  • Expiry. 120 s for reCAPTCHA v2, 300 s for hCaptcha, and Turnstile tokens live until the challenge page is consumed.

The arithmetic that follows is unpleasant. A solve costs you one task and one unit of latency, and it must complete inside a window you do not control:

import math
import random


def gauss(rng, mu=0.0, sigma=1.0):
    u1 = 1.0 - rng.random()
    u2 = rng.random()
    return mu + sigma * math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)


SITE = {
    "token_ttl_s": 120.0,
    "verify_latency_s": 0.9,
    "replayed_token_p": 0.05,
}
SOLVER = {
    "solve_mu_s": 56.0,
    "solve_sigma_s": 27.0,
    "queue_mu_s": 3.0,
    "queue_sigma_s": 2.5,
    "misread_p": 0.09,
}

TRIALS = 400
rng = random.Random(20260903)
ages = []
accepted_ages = []
tally = {"accepted": 0, "bad_token": 0, "duplicate": 0, "expired": 0}

for _ in range(TRIALS):
    solve_s = max(6.0, gauss(rng, SOLVER["solve_mu_s"], SOLVER["solve_sigma_s"]))
    queue_s = max(0.2, gauss(rng, SOLVER["queue_mu_s"], SOLVER["queue_sigma_s"]))
    age = queue_s + solve_s + SITE["verify_latency_s"]
    ages.append(age)
    roll = rng.random()
    if age > SITE["token_ttl_s"]:
        tally["expired"] += 1
    elif roll < SOLVER["misread_p"]:
        tally["bad_token"] += 1
    elif roll < SOLVER["misread_p"] + SITE["replayed_token_p"]:
        tally["duplicate"] += 1
    else:
        tally["accepted"] += 1
        accepted_ages.append(age)

ages.sort()
accepted_ages.sort()


def pct(seq, p):
    return seq[int(round(p * (len(seq) - 1)))]


print("reCAPTCHA v2 submit loop, 400 solves, one egress IP, seeded")
print("token ttl                 %.0f s" % SITE["token_ttl_s"])
print("solve latency p50 / p90   %.1f s / %.1f s" % (pct(ages, 0.50), pct(ages, 0.90)))
print("solve latency p99         %.1f s" % pct(ages, 0.99))
print("headroom left at p90      %.1f s" % (SITE["token_ttl_s"] - pct(ages, 0.90)))
print("")
for key in sorted(tally):
    print("  %-9s %4d  %5.1f%%" % (key, tally[key], 100.0 * tally[key] / TRIALS))
print("")
print("accepted token age p50    %.1f s" % pct(accepted_ages, 0.50))
print("wasted solves             %d of %d" % (TRIALS - tally["accepted"], TRIALS))
print("solver tasks per accepted %.2f" % (TRIALS / float(tally["accepted"])))
reCAPTCHA v2 submit loop, 400 solves, one egress IP, seeded
token ttl                 120 s
solve latency p50 / p90   63.5 s / 92.0 s
solve latency p99         123.4 s
headroom left at p90      28.0 s

  accepted   333   83.2%
  bad_token   36    9.0%
  duplicate   24    6.0%
  expired      7    1.8%

accepted token age p50    63.3 s
wasted solves             67 of 400
solver tasks per accepted 1.20

Two numbers matter more than the rest. The p90 solve latency of 92 s against a 120 s ttl leaves 28 s of slack at the 90th percentile — any queueing delay in your own pipeline eats it. And 1.20 tasks per accepted submission means your budget per successful form post is 20% above the sticker price before you count proxy cost, token-retry logic, or the flow aborts you cause. If the token has to be fetched after the form is already rendered, you will spend the tail of that distribution. Pre-fetch, then commit.

Why Outsourcing the Solve Leaks the Key#

The site_key is public by design — it ships in every page's markup and you can read it without credentials. But the pair of (site key, your request metadata) is not, and every task you create hands the vendor a correlated record:

  • the target hostname and the exact URL, including query string and any token in it;
  • your solve rate, your hour-of-day curve, and how many tasks you have ever submitted for that site key;
  • the proxy exit IP you specified, which is an IP you care about;
  • for proxied tasks, whatever headers and cookies your task payload included.

That record is what a solver's abuse team, or an adversary who has compromised the account, correlates against the target's own logs. The target does not need to break the vendor to see you: it only needs to notice that 40% of the challenges for its site key are being solved from three source networks while zero humans are solving them from the reader base. At that point the site key itself becomes worthless to you, because the origin will refuse every token it issues regardless of validity — the token says "a human passed this," and the origin also knows "only a bot farm passed this."

The deeper leak is behavioural. A proxied solve means a human, or a model, watched the challenge in a browser whose JS telemetry belongs to somebody else. That telemetry is generated for the vendor's environment, not yours, and a vendor that is also a fingerprinting vendor has both. When you must use a solve service, the minimum-egress design is: a browserless task type, no cookies, a proxy drawn from the disposable pool rather than the pool you scrape from, and a rotation of the task payload identity so the vendor's record of your hostname does not accumulate into a census.

Scoring Thresholds Are Where the Money Goes#

For the scored families the decision is a threshold on a 0.0 to 1.0 score, evaluated per action and per hostname. Google's v3 documentation puts the practical bands at roughly 0.9 for "very likely a human", 0.7 for "probably a human", and 0.3 as the usual pass/fail line, and every site sets its own. Turnstile is graded differently — it emits an outcome token whose values the client can read directly, and its scoring blends browser telemetry with IP and site-reputation.

The economics follow from which side of the threshold you sit on, not from the score itself:

  • Above the bar, every request is a normal request. Cost per row approaches the cost of a bare HTTP fetch.
  • Between 0.3 and 0.7 you get intermittent challenges. This is the worst zone to operate in: you pay solver costs on a random fraction of requests, you add the state machine complexity of challenge recovery, and your latency distribution acquires a bimodal tail that the origin can also see.
  • Below 0.3 you are never issued a usable token and no amount of spending helps. The fix is upstream in the session, in the IP, and in the fingerprint.

So the only economically sane posture is to stay above the bar and treat any challenge you see as a leading indicator that you have fallen out of it, not as a task to queue.

Grid and Slider Captchas Are a Local Problem#

Text, image-grid and slider challenges grade pixels and trajectories. They have no network dependency, no per-task fee, and no expiry window, which makes them dramatically cheaper per solve than anything in the table above. The cost is engineering: grid challenges need thresholding, connected-component analysis and a decision rule, and slider challenges need a two-stage controller that produces a trajectory with human velocity profiles rather than a constant-velocity pan. Both are solvable offline in a few hundred milliseconds on a CPU, and neither needs a residential proxy unless the target rate-limits by IP rather than by challenge.

The corollary matters for architecture: if a target serves image or slider challenges, do not route them through a paid solver. Reserve the solver budget for the challenge you cannot compute, and keep the local path as the default so you only spend money on the hard 5% of requests. The geometry and thresholding techniques are covered in detail in .

Cost Per Accepted Submission Is the Only Number#

Solver pricing is per solve, which is the wrong unit. The unit that matters is cost per accepted submission, and the ratio between the two is where the money goes.

From the loop above: 400 solves bought 333 accepted submissions, a ratio of 1.20. That is a good number. A naive pipeline — solve every challenge the moment it appears, from whatever IP the request came in on — routinely runs 2.5 to 4, because it pays full price for expired tokens, duplicated submissions caused by retry races, and tokens that the site rejects on a score basis no amount of solving would have fixed. Multiplied across a fleet, the difference between 1.2 and 3.0 is the entire margin.

The reporting structure that makes this auditable is per-outcome rather than aggregate:

Metric What it tells you
solves per accepted the headline efficiency number
bad-token rate model quality against this site, per site
duplicate rate concurrency bug in your own code
expired rate latency distribution versus the site's TTL
challenged-again rate after acceptance score-based, not solve-based, rejection

The fifth row is the one that reframes the problem. A site that accepts the token and then challenges the next request is telling you the token was never the problem — the score was. Continuing to pay for solves in that case is the most common way teams discover, months later, that they have been buying the wrong product. Escalation ladders () and per-vendor pricing both come back to this table, and the diagnostic workflow is in .

CAPTCHAs are not security controls; they are discrimination with a scoring interface, and that is treated as a legal fact rather than a marketing complaint. The clearest precedent is the 2018 US District Court ruling in Cunningham v. Hoegaarden (No. 17-cv-02511), which held that a municipality's use of a CAPTCHA on its own website violated Title III of the ADA because the site was not a place of public accommodation — a reasoning that vendors have since pushed toward sites that are. Accessibility suits have since been filed against the major vendors, and the technical consensus is that reCAPTCHA's audio challenge is not reliably usable by blind users, which is the specific condition the ADA analysis turns on.

Three practical consequences for how you scope a project:

  • Every challenge you solve is a challenge a person failed to solve, or was denied access to. Automation at volume is a denial-of-service attack on an accessibility affordance, regardless of legal exposure.
  • Terms of service for the solver vendors themselves forbid using their service to violate a third party's terms. That is a contractual chain that makes your account the weak link in any enforcement action.
  • Some jurisdictions treat automated circumvention of access controls as a circumventing-access-device question with criminal exposure attached. The full survey is in , and it belongs in your scoping conversation rather than in a later compliance review.

Treat the solver as a costed dependency with a rate limit and an exit, not as infrastructure. If the cheapest way to get the data is a technique you would not defend in public, that is a signal about the target, not a hurdle to engineer through.