๐ŸŽ‰ Premium Proxies ยท 24-Hour Free TrialClaim Now
Guides

CAPTCHA Keeps Coming Back After Solving: How to Fix It

CAPTCHA keeps coming back after solving? The clearance is tied to the IP, User-Agent and TLS that solved it. Solve on a sticky session and reuse the cookies.

S SparkProxy 3 19 min read
Share

When a CAPTCHA keeps coming back after solving, the solve usually worked. What failed is everything after it: the token arrived late or twice, or the clearance cookie got sent from a different exit IP, a different User-Agent or a different TLS handshake than the one that earned it. A site that grants a pass grants it to an identity, and a scraper that rotates right after the solve throws that identity away. Here's how to find which part broke, cheapest check first, and the order of operations that makes a solve last until its cookie expires.

CAPTCHA Keeps Coming Back After Solving: The Two Loops

The loop takes two forms, and they have different causes, so first work out which one you have.

Token-style challenges (reCAPTCHA, hCaptcha, a standalone Turnstile widget). Your solver returns a token, you submit the form, and the page renders the widget again or returns a "verification failed" message. If you control the backend or can see its logs, the verify call usually names the reason. These are the verbatim error codes from each vendor's own documentation, read on 2026-10-30:

reCAPTCHA   timeout-or-duplicate      "The response is no longer valid: either is too old or has been used previously."
Turnstile   timeout-or-duplicate      "Token has already been validated."
hCaptcha    already-seen-response     "The response parameter (verification token) was already verified once."
hCaptcha    expired-input-response    "The response parameter (verification token) is expired. (120s default)"

Clearance-style challenges (Cloudflare's interstitial, the "Just a moment..." page). The solve succeeds and you get a cf_clearance cookie. The first follow-up request passes, or doesn't, and within a request or two you're back on the interstitial with a 403. Nothing tells you why. Cloudflare's own docs describe the cookie as one that "stores the proof of challenge passed", and that's all a client ever sees.

If you send traffic through the SparkProxy Scraping API, the loop surfaces as a documented HTTP 530 with "reason": "captcha_blocked" and a captcha_type such as cloudflare_turnstile, hcaptcha or recaptcha. Jump to the API section for that case.

A Pass Belongs to the Identity That Earned It

Every fix below follows from one idea. A solved challenge isn't a pass for "your scraper". It's a pass for the specific combination of exit IP, User-Agent, TLS fingerprint and cookie jar that was present when it was solved. Change any of those and, from the site's point of view, a stranger is holding someone else's ticket.

Be precise about what's documented and what isn't. Cloudflare doesn't publish the exact inputs it checks a cf_clearance against. Its docs give the lifetime ("By default, the cf_clearance cookie has a lifetime of 30 minutes", adjustable via Challenge Passage) and say that "If the cookie is still valid, no challenges will be shown." The binding is described most plainly by the solver vendors, whose business depends on getting it right. CapSolver's own AntiCloudflareTask guide, read on 2026-10-30, says:

  • "Proxy is necessary, please use static proxy or sticky proxy instead of rotating proxy"
  • "Support custom userAgent, please keep it the same as the one you are using."
  • "You have to use the TLS request library to request the target website."

2Captcha's Turnstile docs say the same about the browser string for Cloudflare challenge pages: you must "use the userAgent returned from our API." Neither vendor would demand all three if the cookie worked from anywhere.

Token-style CAPTCHAs add a clock. Google states that "Each reCAPTCHA user response token is valid for two minutes, and can only be verified once." Cloudflare gives Turnstile tokens a "Validity period: 300 seconds (5 minutes) from generation" and says "Tokens are single-use." hCaptcha's default is 120 seconds. All three verify APIs also accept the visitor's IP as remoteip (optional for Google and Cloudflare, "Recommended" for hCaptcha), so a site can compare the IP that solved with the IP that submitted.

ChallengeWhat you getLifetime (vendor docs, 2026-10-30)Reusable?Tied to
reCAPTCHA v2/v3Response token2 minutesNo, one verifySite's choice; `remoteip` optional
Turnstile widgetResponse token300 secondsNo, single-useSite's choice; `remoteip` optional
hCaptchaResponse token120 s defaultNo, one verify`remoteip` recommended
Cloudflare challenge page`cf_clearance` cookie30 min default, site-configurableYes, until expirySolver vendors: same proxy, UA, TLS
Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Triage in Cost Order

Run these in order. The first two cost almost nothing; the later ones need a solve to test.

#CheckCostA positive result means
1Token age and reuseLog two timestampsYou submitted a stale or replayed token
2Exit IP before vs after the solveTwo probe requestsYour proxy rotated between solve and reuse
3UA and TLS on the follow-up requestOne request, inspect headersThe follow-up client isn't the one that solved
4Cookies present on the follow-upPrint the outgoing jarThe clearance never left your process
5Still looping with all four cleanA few requestsBehavioural or rate trigger; see [below](#still-challenged)

Check 1: Did the Token Arrive Fresh, Once?

Solver queues are slow when they're busy, and a token that spends 90 seconds in a queue has 30 left on reCAPTCHA's clock. Retries make it worse: an HTTP client that retries a failed POST resubmits the same token, which is exactly what timeout-or-duplicate and already-seen-response are there to catch.

Stamp the token when the solver hands it over and refuse to submit anything past a safety margin:

import time

TTL = {"recaptcha": 120, "hcaptcha": 120, "turnstile": 300}   # seconds, per vendor docs
MARGIN = 20                                                    # leave room for the POST itself

class Token:
    def __init__(self, kind: str, value: str):
        self.kind, self.value = kind, value
        self.issued = time.monotonic()
        self.used = False

    def take(self) -> str:
        age = time.monotonic() - self.issued
        if self.used:
            raise RuntimeError("token already submitted once; solve again")
        if age > TTL[self.kind] - MARGIN:
            raise RuntimeError(f"token is {age:.0f}s old; solve again")
        self.used = True
        return self.value

Two related mistakes show up as the same loop. The first is putting the token into g-recaptcha-response (or h-captcha-response, or cf-turnstile-response) but never firing the page's callback, so the site's JavaScript never submits it. 2Captcha's docs spell this out for challenge pages: intercept the callback and "execute the callback passing the token as it's argument." The second is solving with the wrong sitekey or on the wrong page. Google's verify response includes hostname, "the hostname of the site where the reCAPTCHA was solved", so a token solved against another host can be refused even when it's fresh.

Check 2: Did the Exit IP Move After the Solve?

This is the most common cause, and it's the one the organising idea predicts. On a Random Rotation endpoint, every new connection can leave from a different address. The solve happens on one exit and the follow-up arrives from another, carrying a clearance that was issued to the first.

Probe the exit on both sides of the solve:

import requests

ROTATE = "http://USER:PASS@gateway.sparkproxy.io:11000"   # Random Rotation
STICKY = "http://USER:PASS@gateway.sparkproxy.io:11002"   # Sticky Sessions, up to 5 minutes

def exit_ip(proxy: str) -> str:
    r = requests.get("https://api.ipify.org",
                     proxies={"http": proxy, "https": proxy}, timeout=20)
    return r.text.strip()

for name, proxy in (("rotate", ROTATE), ("sticky", STICKY)):
    before = exit_ip(proxy)
    # ... your solve would happen here ...
    after = exit_ip(proxy)
    print(f"{name:6} before={before} after={after} same={before == after}")

If the rotating line shows two addresses, you've found the loop. Move both the solve and every request that reuses its result onto Sticky Sessions. Our explainer on what a sticky session proxy is covers the session model, so here are only the two details that matter for CAPTCHAs.

First, the sticky window caps the clearance. A cf_clearance defaults to 30 minutes, but SparkProxy's Sticky Sessions hold an exit for up to 5 minutes. Since the pass is bound to the exit, its useful life is whichever ends first: the cookie or the exit. Plan on roughly one solve per sticky window, and fit as many pages into that window as the site tolerates.

Second, a remote solver has to share your exit. With CapSolver or 2Captcha you pass your proxy into the task so the solve happens on the same IP you'll reuse. That needs user:pass authentication, because the solver's servers aren't on your IP whitelist. Probe your exit right before creating the task, and treat a challenge on the very first reuse as a sign the two sides didn't land on the same address.

Check 3: Do the User-Agent and TLS Still Match?

The classic version of this bug: a headless Chrome solves the challenge, the scraper copies the cookies into requests, and the next request goes out with python-requests/2.34.2 as its User-Agent and Python's OpenSSL handshake. The cookie is right; the client presenting it is obviously not the browser that earned it.

Fix both layers. Take the User-Agent from wherever the solve happened (the browser, or the userAgent field your solver returns), and send it from a client whose TLS handshake looks like that browser. curl_cffi (0.17.0 on PyPI, uploaded 2026-10-26) does the second part with impersonate. Its docs, read on 2026-10-30, list chrome152 as the newest Windows target and say "The chrome alias selects the Windows profile."

from curl_cffi import requests as creq

STICKY = "http://USER:PASS@gateway.sparkproxy.io:11002"

def reuse_client(user_agent: str, cookies: dict) -> creq.Session:
    s = creq.Session(impersonate="chrome")
    s.proxies = {"http": STICKY, "https": STICKY}
    s.headers.update({"User-Agent": user_agent})
    for name, value in cookies.items():
        s.cookies.set(name, value, domain=".sparkproxy.io")
    return s

Keep the claimed browser consistent. A Windows Chrome UA string on a macOS impersonation profile, or a Chrome 152 handshake under a Firefox UA, is another mismatch. If you solved in Playwright's bundled Chromium, read its real UA with page.evaluate("navigator.userAgent") rather than typing one in. Our post on TLS fingerprinting explains what the handshake gives away.

Check 4: Are the Cookies Actually Carried Forward?

Sometimes the clearance simply never leaves the process that solved it. Typical leaks:

  • The browser context is closed and its cookies go with it.
  • Cookies are copied without a domain, or with the wrong one, so the HTTP client never sends them.
  • Each worker builds a fresh session, so only the one that solved holds the cookie.
  • The site set its own session cookie before the challenge, and that cookie got dropped while cf_clearance was kept.

Copy the whole jar, not just the cookie you recognise. Cloudflare also sets __cf_bm, which its docs say "expires after 30 minutes of continuous inactivity", and many sites tie their own session cookie to the same visit. Playwright's storage_state() exports everything, domains included:

import json
from playwright.sync_api import sync_playwright

PROXY = {"server": "http://gateway.sparkproxy.io:11002",
         "username": "USER", "password": "PASS"}

with sync_playwright() as p:
    browser = p.chromium.launch(proxy=PROXY)
    ctx = browser.new_context()
    page = ctx.new_page()
    page.goto("https://www.sparkproxy.io/pricing", wait_until="networkidle")
    state = ctx.storage_state()
    ua = page.evaluate("navigator.userAgent")
    browser.close()

with open("identity.json", "w") as fh:
    json.dump({"ua": ua, "cookies": state["cookies"]}, fh)

for c in state["cookies"]:
    print(c["name"], c["domain"], c["expires"])

Print the outgoing headers on the first reuse request and confirm the Cookie: line contains everything from that list. Our guide to handling cookies and sessions in scrapers covers jar mechanics in depth.

The Working Order: Solve Sticky, Reuse Until Expiry

Put the four checks together and the correct sequence is short:

  1. Open a Sticky Sessions connection and record the exit IP.
  2. Solve on that exit, in your browser or by passing the same proxy to your solver.
  3. Capture every cookie plus the exact User-Agent.
  4. Reuse them from a browser-matching client on the same sticky exit.
  5. Re-solve when the exit changes, the cookie expires, or a challenge reappears. Not before.

Here's that loop with CapSolver's documented AntiCloudflareTask as the solver. The endpoints, the ip:port:user:pass proxy format and the solution fields (cookies, token, userAgent) come from its guide; the guide also recommends a session of at least 3 minutes, which the 5-minute sticky window covers.

import time
import requests
from curl_cffi import requests as creq

KEY = "YOUR_CAPSOLVER_KEY"
USER, PASS = "USER", "PASS"
STICKY = f"http://{USER}:{PASS}@gateway.sparkproxy.io:11002"
STICKY_SOLVER = f"gateway.sparkproxy.io:11002:{USER}:{PASS}"
TARGET = "https://www.sparkproxy.io/pricing"

def solve_clearance(url: str) -> dict:
    task = {"type": "AntiCloudflareTask", "websiteURL": url, "proxy": STICKY_SOLVER}
    r = requests.post("https://api.capsolver.com/createTask",
                      json={"clientKey": KEY, "task": task}, timeout=30).json()
    if r.get("errorId"):
        raise RuntimeError(r.get("errorDescription"))
    for _ in range(30):
        time.sleep(3)
        res = requests.post("https://api.capsolver.com/getTaskResult",
                            json={"clientKey": KEY, "taskId": r["taskId"]}, timeout=30).json()
        if res.get("status") == "ready":
            return res["solution"]
        if res.get("errorId"):
            raise RuntimeError(res.get("errorDescription"))
    raise TimeoutError("solver did not finish")

def looks_challenged(resp) -> bool:
    return resp.status_code in (403, 503) and "Just a moment" in resp.text

class Identity:
    """One sticky exit, one UA, one cookie jar. Replaced as a unit."""

    def __init__(self):
        self.s = creq.Session(impersonate="chrome")
        self.s.proxies = {"http": STICKY, "https": STICKY}
        self.ip = self.s.get("https://api.ipify.org", timeout=20).text.strip()
        sol = solve_clearance(TARGET)
        self.s.headers.update({"User-Agent": sol["userAgent"]})
        for name, value in sol.get("cookies", {}).items():
            self.s.cookies.set(name, value, domain=".sparkproxy.io")
        self.born = time.monotonic()

    def expired(self) -> bool:
        return time.monotonic() - self.born > 270    # under the 5-minute sticky window

def crawl(urls):
    ident = Identity()
    for url in urls:
        if ident.expired():
            ident = Identity()
        resp = ident.s.get(url, timeout=30)
        if looks_challenged(resp):
            ident = Identity()                        # pass is dead: replace everything
            resp = ident.s.get(url, timeout=30)
        yield url, resp.status_code

Notice what the code never does: it never rotates the IP while keeping the cookies, and it never keeps the cookies on a new IP. The exit, the UA and the jar live and die together. The 270-second refresh assumes the sticky exit was assigned around the moment the Identity was built; if your logs show a challenge before then, probe the exit (as in Check 2) instead of trusting the timer.

When the Challenge Returns Anyway

If tokens are fresh, the exit holds, the UA and TLS match and the jar travels, but the challenge still comes back, the site is challenging what you do rather than who you are. Three patterns account for most of it:

  • Rate. A clearance doesn't buy unlimited requests. Firing 40 requests a second from one freshly cleared identity looks nothing like the visitor who solved, and the site can re-challenge that session on its own rules. Slow the identity down and spread load across several.
  • Behaviour on rendered pages. On DataDome, PerimeterX or Akamai, the SparkProxy docs note that captcha_type is "null for behavioral blocks", which tells you no CAPTCHA is involved at all. A solver can't help with those. Our guide on how to detect when your scraper is blocked shows how to tell the block types apart.
  • The site's own rules. Cloudflare warns that "Using Cloudflare Challenges along with Rules features may cause challenge loops." If a custom rule challenges every request to a path, no client fix ends it. That's a loop on the operator's side.

If you're solving dozens of challenges per hour, the better fix is upstream: get challenged less often. That ground is covered in our guide on how to avoid CAPTCHAs when web scraping, and if you're picking a solver, our solver buying guide compares pricing and challenge coverage.

Which Causes a Proxy Settles

CauseCan a proxy fix it?The real fix
Exit IP rotated between solve and reuseYesSolve and reuse on Sticky Sessions
Solver solved on its own IPYesPass your sticky proxy (user:pass) into the task
Clearance outlived the sticky exitPartlyRe-solve each sticky window
Token expired or resubmittedNoSubmit within TTL, once; never retry the POST with the same token
Callback never fired, wrong sitekeyNoFire the page callback; solve on the exact page
UA or TLS mismatch on reuseNoReuse the solver's UA from an impersonating client
Cookies dropped between solve and reuseNoExport and carry the full jar
Rate or behavioural re-challengePartly; spreads loadLower per-identity rate, render properly
Site rule that challenges every requestNoNothing client-side

Three rows out of nine are things a proxy settles on its own. The rest are client bugs, and a bigger IP pool makes none of them better.

Offloading It to the Scraping API

If you'd rather not run the solve-and-reuse loop yourself, the SparkProxy Scraping API's docs say "Cloudflare, CAPTCHAs, and JS rendering handled automatically." Treat that as our own product claim and test it against your targets with the 1,000 free credits. The base URL is https://scrape.sparkproxy.io/api/v1 with the key in an X-API-Key header. render_js defaults to true and costs 5 credits; country_code adds 5.

One behaviour matters for this topic. In rendered mode the docs state that "Cookies and login state do not persist across separate API requests", and each request gets a fresh browser profile whatever session_id you send. Each call has to clear its own challenge, so you can't solve once and reuse across calls. If you already hold valid cookies, the cookies parameter (rendered mode) injects them before the page loads, and custom_ua sets the User-Agent; the reuse rules above still apply.

Use json_response=true to get the envelope (job_id, status_code, duration_ms, credits_used, body), and decode body from base64 before you look for challenge markers:

import base64
import requests

r = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "url": "https://www.sparkproxy.io/pricing",
        "render_js": "true",          # the default; 5 credits
        "proxy_type": "premium",
        "json_response": "true",      # the envelope; body is base64
    },
    timeout=120,
)

if r.status_code == 530:
    err = r.json()
    print(err.get("reason"), err.get("captcha_type"), err.get("attempts"))
else:
    env = r.json()
    html = base64.b64decode(env["body"]).decode("utf-8", "replace")
    print(env["status_code"], env["credits_used"], "Just a moment" in html)

A 530 with "reason": "captcha_blocked" comes with the documented error string "CAPTCHA challenge (cloudflare_turnstile) could not be bypassed with the current proxy". The docs say failed requests are refunded automatically, so a loop here costs time rather than credits.

For self-run pipelines on raw proxies, plans start from $75/month (USD, read from sparkproxy.io on 2026-10-09), sold as packs with unlimited bandwidth, so an extra re-solve request doesn't add traffic charges. You get 1M+ premium IPs in 80+ countries, Random Rotation and Sticky Sessions, HTTP/HTTPS/SOCKS5, user:pass or IP whitelist auth (use user:pass when a remote solver shares your exit), and a 24-hour free trial with no credit card.

Frequently asked questions

FAQ

Usually because the follow-up request isn't coming from the identity that solved it. The exit IP rotated, the User-Agent or TLS fingerprint changed, the cookies weren't carried over, or a token expired or was submitted twice. Solve and reuse on one sticky exit with the same UA and full cookie jar.

Cloudflare doesn't publish what it checks the cookie against, but solver vendors such as CapSolver require a static or sticky proxy, the same User-Agent and a TLS-impersonating client. A cookie sent from a new IP, a different UA or a plain Python TLS stack is commonly rejected.

Per each vendor's docs: reCAPTCHA tokens last two minutes, Turnstile tokens 300 seconds, and hCaptcha tokens 120 seconds by default. All three are single-use. Cloudflare's cf_clearance cookie defaults to 30 minutes and site owners can change that.

It's the error reCAPTCHA and Turnstile return when a token is too old or has already been verified. Solve again and submit the new token once, inside its lifetime, without letting an HTTP retry resend it.

Sticky. A clearance is bound to the exit that earned it, so the solve and every reuse should share one exit. SparkProxy's Sticky Sessions hold an exit for up to 5 minutes, which also caps how long a clearance stays useful.

Only partly. A sticky proxy fixes the IP-change causes, but stale tokens, an unfired callback, a UA or TLS mismatch, and dropped cookies are client bugs that no proxy fixes.

Special Discount ยท 20% off

Get 20% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Save up to 15% more on quarterly, half-yearly and yearly plans

Claim Discount

About the Author

This guide comes from the SparkProxy Technical Team, the engineers behind SparkProxy's datacenter proxy network and its Scraping API. Every token lifetime, error string and solver requirement above was read from the vendor's own documentation on 2026-10-30, and where a vendor doesn't document something (such as what Cloudflare binds a clearance to), we've said so instead of guessing.

Keep reading

Related articles