Cloudflare cf_clearance: why it expires and how to stop the re-challenge loop
You solve the Cloudflare challenge, get a cf_clearance cookie, make your next request — and Cloudflare throws you straight back to the challenge. You solve it again, same thing. The scraper is technically "working," it's just stuck in a loop, burning a solve on every request and never getting to the actual page.
This is one of the most common Cloudflare problems, and it's almost never the challenge itself. It's how cf_clearance is issued and what it's tied to. Once you understand that, the loop has a clean fix.
What cf_clearance actually is
When you pass a Cloudflare challenge (managed challenge, JS challenge, or interactive Turnstile), Cloudflare issues a cf_clearance cookie. That cookie is your proof of passage — present it on subsequent requests and Cloudflare lets you through without re-challenging.
The catch is that cf_clearance is not a portable token you can reuse anywhere. It carries two constraints that cause the loop:
1. It has a TTL
cf_clearance expires. The window varies by site configuration (often tens of minutes, sometimes shorter under stricter settings or "Under Attack" mode). When it expires, the next request gets challenged again — expected behavior. The bug is when your code keeps replaying an expired cookie instead of noticing it's dead and minting a fresh one.
2. It's bound to your IP + User-Agent + TLS fingerprint
This is the one that traps people. Cloudflare binds the clearance to the exact context that solved the challenge:
- the IP that passed it,
- the User-Agent string presented, and
- the TLS/JA3 fingerprint of the client.
Change any of those between getting the cookie and using it, and Cloudflare treats the cookie as invalid and re-challenges. That's why these patterns loop forever:
- Rotating proxies mid-session — you solve on IP A, your next request goes out IP B, cookie rejected.
-
Solving in a real browser, then submitting from
requests— the browser's TLS fingerprint and UA don't match your HTTP client, so the cookie you carefully obtained is dead on the first reuse. -
Spoofing a UA that doesn't match the fingerprint — claiming Chrome while sending a Python-
requestsJA3 is itself a mismatch.
How to confirm it's the loop and not something else
Before fixing, log what you're actually getting back:
import requests
r = requests.get(url, headers=headers, cookies={"cf_clearance": clearance})
print(r.status_code)
print(r.headers.get("cf-mitigated")) # "challenge" => you got re-challenged
print("cf_clearance" in r.cookies) # did this response set a NEW one?
-
cf-mitigated: challenge(or a challenge-page body) → you're in the re-challenge loop; the cookie was rejected. - A flat
403with a Cloudflare1006/1007error code → that's an IP ban, not a clearance problem; no cookie fixes it (you need a different egress IP). - A clean
200→ you're through; the cookie is valid for now.
The fix
The loop breaks when you stop reusing a stale/mismatched cookie and instead evict-and-re-mint on the challenge signal, with a pinned context:
-
Pin one IP + one UA + one TLS fingerprint per session. Don't rotate the proxy mid-session. If you're submitting from Python, send a browser-matching TLS fingerprint (e.g.
curl_cffiwithimpersonate) and the same UA you solved with — not the defaultrequestsfingerprint. -
Detect re-challenge by the response, not a timer. Cloudflare can invalidate early, so don't trust a fixed "valid for N minutes" assumption. When you see
cf-mitigated: challenge, treat the stored clearance as dead, evict it, and re-mint. - Re-mint with the same context that will reuse it. Whatever solves the challenge (a real browser or a solver) must produce a cookie usable by the same IP+UA+fingerprint that makes the real requests.
- Optionally refresh proactively just before the known TTL to avoid a user-facing stall.
A minimal session loop that respects all of this:
import curl_cffi.requests as cc
session = cc.Session(impersonate="chrome") # browser-matching TLS fingerprint
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/124.0 Safari/537.36"
session.headers["User-Agent"] = UA
PROXY = "http://user:pass@residential-ip:port" # one sticky IP for the session
session.proxies = {"http": PROXY, "https": PROXY}
def get(url, clearance):
r = session.get(url, cookies={"cf_clearance": clearance} if clearance else {})
if r.headers.get("cf-mitigated") == "challenge" or "challenge-platform" in r.text:
clearance = mint_clearance(url, UA, PROXY) # re-mint with the SAME UA+IP
r = session.get(url, cookies={"cf_clearance": clearance})
return r, clearance
The point isn't the exact library — it's that the cookie, the UA, the IP, and the fingerprint all stay consistent, and a challenge response triggers a re-mint instead of another doomed retry.
If you offload the challenge to a solving service, the same rule applies: it has to mint the clearance against the IP+UA you'll reuse, or hand back a token you submit immediately in a matched context. CaptchaAI handles the Cloudflare challenge flow this way and is 2Captcha-API-compatible, so an existing client is mostly a base-URL change — and if you want to test it against your own target, the trial is free (3 days, no card).
TL;DR checklist
- [ ] One IP + one UA + one TLS fingerprint, pinned for the whole session
- [ ] Don't rotate the proxy mid-session
- [ ] Submit with a browser-matching TLS fingerprint (not raw
requests) if you solved in a browser - [ ] Detect re-challenge via
cf-mitigated/ challenge body, evict + re-mint — don't replay a dead cookie - [ ] Rule out a 1006/1007 IP ban first (different problem, needs a new IP)
Get those right and the loop turns into a single solve followed by clean 200s — which is how it's supposed to behave.
Top comments (0)