Cloudflare WAF and 5-second challenges
More
Some sites answer a first visit with a Cloudflare challenge page instead of the page itself. It goes by several names, all one thing to solve:
- A Cloudflare WAF challenge: a WAF rule (a custom, rate limiting or IP Access rule) whose action is Managed Challenge, Non-Interactive Challenge or Interactive Challenge. Bot Fight Mode and Under Attack mode show the same page.
- “Just a moment…” or “Checking your browser”: the interstitial page itself, answered with
HTTP 403 and the header
cf-mitigated: challenge. - The 5-second challenge: the old name for Cloudflare’s JavaScript challenge, after the few
seconds its page took. Today it is a Managed or Non-Interactive Challenge (
js_challenge), which Cloudflare says typically takes less than five seconds.
A CloudflareChallengeTask passes that page for you and returns what a browser gets for passing it:
the cf_clearance cookie, and the user agent the cookie is bound to. Send both with your requests
to the site, through the same proxy, and the site serves its pages. A Cloudflare block, such as
error 1020, is a refusal rather than a challenge: no task passes it.
A challenge task is priced, held and charged like any other task: the price shows in the price list, and nothing is charged unless the task is solved.
What the clearance is
Section titled “What the clearance is”cf_clearance is the cookie Cloudflare sets when a visitor passes a challenge. While it is valid,
the site lets that visitor through without challenging it again. Cloudflare ties it to “the
specific visitor and device it was issued to”, so in practice it is accepted only:
- from the same IP address that earned it: so a challenge task always runs through your proxy, and you use the clearance through that proxy;
- with the same user agent that earned it: the
User-Agentheader insolution.userAgent, exactly; - for as long as the site allows: its Challenge Passage setting, 30 minutes by default. We serve
the clearance for 30 minutes after it is issued (
tokenExpiresAt), then delete it 10 minutes later.
A client whose TLS handshake does not look like the browser the user agent names may be challenged again, whatever its cookie: Cloudflare’s bot detection looks at TLS fingerprints as well as headers. A plain HTTP library’s handshake does not look like Chrome’s. For sites that check, use a client that impersonates the browser, such as curl-impersonate, or a real browser.
Why it needs your proxy
Section titled “Why it needs your proxy”A cookie earned from our solver’s address would be refused from yours, so there is no proxyless
challenge task: a CloudflareChallengeTaskProxyless is refused, with
validation_failed on REST and
ERROR_TASK_NOT_SUPPORTED in the createTask format, and nothing is held. Your proxy also looks the
page up and connects to it, so the solve comes from the address you will use.
Create a task
Section titled “Create a task”POST /v1/tasks with the page and your proxy. A challenge page has no widget, so the task takes
no websiteKey, action or cdata; sending one is refused.
| Field | Required | What it is |
|---|---|---|
type |
Yes | CloudflareChallengeTask. CapSolver’s name, AntiCloudflareTask, works too. |
websiteURL |
Yes | The page behind the challenge: a public http or https page. |
proxy |
Yes | Your proxy, http or https, with its port, such as http://user:pass@proxy.example.net:8080. The same rules apply as for TurnstileTask: a public host, and a port no other protocol reserves. |
callbackUrl |
No | Where to POST the result when the task ends. See Polling and callbacks. |
The same checks as a Turnstile task apply before the task is held and again before each attempt: the page must be on a public domain and not on the blocklist, and your proxy must resolve to public addresses only. Your proxy’s password is never logged, and it is deleted when the task ends.
curl "$ZEROCAPTCHA_API/v1/tasks" \ -H "Authorization: Bearer $ZEROCAPTCHA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"type": "CloudflareChallengeTask", "websiteURL": "https://shop.example.com/", "proxy": "http://user:pass@proxy.example.net:8080", "callbackUrl": "https://example.com/zerocaptcha/callback"}'const api = process.env.ZEROCAPTCHA_API;const headers = { Authorization: `Bearer ${process.env.ZEROCAPTCHA_KEY}` };
let task = await fetch(`${api}/v1/tasks`, { method: "POST", headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ type: "CloudflareChallengeTask", websiteURL: "https://shop.example.com/", proxy: process.env.PROXY_URL, // such as http://user:pass@proxy.example.net:8080 callbackUrl: "https://example.com/zerocaptcha/callback", // optional: POSTed when the task ends }),}).then((response) => response.json());
while (task.status === "queued" || task.status === "running") { await new Promise((resolve) => setTimeout(resolve, 2000)); task = await fetch(`${api}/v1/tasks/${task.id}`, { headers }).then((response) => response.json());}if (!task.solution) throw new Error(`${task.errorCode}: ${task.errorDescription}`);const { userAgent, cookie } = task.solution; // cookie.name is "cf_clearance"import osimport timeimport uuid
import requests
api = os.environ["ZEROCAPTCHA_API"]headers = {"Authorization": f"Bearer {os.environ['ZEROCAPTCHA_KEY']}"}
task = requests.post( f"{api}/v1/tasks", headers={**headers, "Idempotency-Key": str(uuid.uuid4())}, json={ "type": "CloudflareChallengeTask", "websiteURL": "https://shop.example.com/", "proxy": os.environ["PROXY_URL"], # such as http://user:pass@proxy.example.net:8080 "callbackUrl": "https://example.com/zerocaptcha/callback", # optional: POSTed when the task ends }, timeout=15,).json()while task["status"] in ("queued", "running"): time.sleep(2) task = requests.get(f"{api}/v1/tasks/{task['id']}", headers=headers, timeout=15).json()if not task.get("solution"): raise SystemExit(f"{task['errorCode']}: {task['errorDescription']}")user_agent = task["solution"]["userAgent"]clearance = task["solution"]["cookie"]["value"]type challengeTask struct { ID string `json:"id"` Status string `json:"status"` ErrorCode string `json:"errorCode"` ErrorDescription string `json:"errorDescription"` Solution *struct { UserAgent string `json:"userAgent"` Cookie struct { Name string `json:"name"` Value string `json:"value"` } `json:"cookie"` } `json:"solution"`}
func solveChallenge(pageURL, proxy string) (*challengeTask, error) { body, _ := json.Marshal(map[string]string{ "type": "CloudflareChallengeTask", "websiteURL": pageURL, "proxy": proxy, // Optional: POSTed when the task ends. "callbackUrl": "https://example.com/zerocaptcha/callback", }) req, _ := http.NewRequest(http.MethodPost, os.Getenv("ZEROCAPTCHA_API")+"/v1/tasks", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("ZEROCAPTCHA_KEY")) req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", fmt.Sprint(time.Now().UnixNano())) var task challengeTask for { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } err = json.NewDecoder(resp.Body).Decode(&task) resp.Body.Close() if err != nil { return nil, err } if task.Status != "queued" && task.Status != "running" { break } time.Sleep(2 * time.Second) req, _ = http.NewRequest(http.MethodGet, os.Getenv("ZEROCAPTCHA_API")+"/v1/tasks/"+task.ID, nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("ZEROCAPTCHA_KEY")) } if task.Solution == nil { return nil, fmt.Errorf("%s: %s", task.ErrorCode, task.ErrorDescription) } return &task, nil}<?php// The createTask format, as CapSolver's clients send it.$body = json_encode([ 'clientKey' => getenv('ZEROCAPTCHA_KEY'), 'task' => [ 'type' => 'AntiCloudflareTask', 'websiteURL' => 'https://shop.example.com/', 'proxy' => getenv('PROXY_URL'), ], // Optional: POSTed when the task ends. 'callbackUrl' => 'https://example.com/zerocaptcha/callback',]);// The same Idempotency-Key on a retry returns the same task instead of a second, paid one.$headers = "Content-Type: application/json\r\nIdempotency-Key: " . bin2hex(random_bytes(16));$context = stream_context_create(['http' => ['method' => 'POST', 'header' => $headers, 'content' => $body]]);$created = json_decode(file_get_contents(getenv('ZEROCAPTCHA_API') . '/createTask', false, $context), true);
do { sleep(2); $poll = json_encode(['clientKey' => getenv('ZEROCAPTCHA_KEY'), 'taskId' => $created['taskId']]); $context = stream_context_create(['http' => ['method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => $poll]]); $result = json_decode(file_get_contents(getenv('ZEROCAPTCHA_API') . '/getTaskResult', false, $context), true);} while ($result['errorId'] === 0 && $result['status'] === 'processing');
$userAgent = $result['solution']['userAgent'];$clearance = $result['solution']['cookies']['cf_clearance'];The SDKs do all of this in one call: solveChallenge in Node, solve_challenge in
Python and SolveChallenge in Go return the clearance and its user agent.
Read the result
Section titled “Read the result”Read the task with GET /v1/tasks/{id} until it ends. A solved one carries the clearance:
{ "id": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "type": "CloudflareChallengeTask", "kind": "cloudflare", "status": "succeeded", "websiteURL": "https://shop.example.com/", "websiteKey": null, "usesProxy": true, "solution": { "token": "Dyw1BhDnEAGRy5fh…", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", "cookie": { "name": "cf_clearance", "value": "Dyw1BhDnEAGRy5fh…", "expiresAt": null } }, "tokenState": "available", "tokenIssuedAt": "2026-09-30T14:02:14Z", "tokenExpiresAt": "2026-09-30T14:32:14Z"}| Field | What it is |
|---|---|
solution.cookie.value |
The cf_clearance cookie’s value. solution.token holds the same value. |
solution.userAgent |
The User-Agent to send with the cookie. |
solution.cookie.expiresAt |
When the site stops accepting the cookie, if it is known; null when it is not, as today. The site’s own setting decides (its Challenge Passage, 30 minutes by default), and the solver does not learn it. |
tokenExpiresAt |
Until when the API serves the clearance: 30 minutes after it was issued. It is deleted 10 minutes after that. |
In the createTask format, getTaskResult answers as CapSolver’s AntiCloudflareTask does:
{ "errorId": 0, "taskId": "0192f3a4-7b1c-7d2e-9f10-3c4d5e6f7a8b", "status": "ready", "solution": { "token": "Dyw1BhDnEAGRy5fh…", "type": "cloudflare", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", "cookies": { "cf_clearance": "Dyw1BhDnEAGRy5fh…" } }, "cost": "0.001200", "createTime": 1790776925, "endTime": 1790776934, "solveCount": 1, "expiresAt": "2026-09-30T14:32:14Z"}A createTask for a challenge page ignores the fields other providers take for one, such as
userAgent and html, and any websiteKey, action or cdata.
Use the clearance
Section titled “Use the clearance”Send the cookie and the user agent with each request to the site, through the proxy that earned them. Reuse them for every request until the site challenges you again, then create a new task.
curl "https://shop.example.com/" \ --proxy "$PROXY_URL" \ -H "User-Agent: $USER_AGENT" \ -H "Cookie: cf_clearance=$CF_CLEARANCE"// got 14, through the same proxy with hpagent.import got from "got";import { HttpsProxyAgent } from "hpagent";
const site = got.extend({ agent: { https: new HttpsProxyAgent({ proxy: process.env.PROXY_URL }) }, headers: { "user-agent": userAgent, cookie: `cf_clearance=${cookie.value}` },});const page = await site("https://shop.example.com/");console.log(page.statusCode);import os
import httpximport requests
proxy = os.environ["PROXY_URL"]
# requestswith requests.Session() as session: session.proxies = {"http": proxy, "https": proxy} session.headers["User-Agent"] = user_agent session.cookies.set("cf_clearance", clearance, domain="shop.example.com") print(session.get("https://shop.example.com/", timeout=30).status_code)
# httpxwith httpx.Client(proxy=proxy, headers={"User-Agent": user_agent}, cookies={"cf_clearance": clearance}) as client: print(client.get("https://shop.example.com/", timeout=30).status_code)// Through the same proxy, with the cookie in a jar and the user agent on every request.proxy, _ := url.Parse(os.Getenv("PROXY_URL"))jar, _ := cookiejar.New(nil)site, _ := url.Parse("https://shop.example.com/")jar.SetCookies(site, []*http.Cookie{{Name: "cf_clearance", Value: task.Solution.Cookie.Value}})client := &http.Client{ Jar: jar, Transport: &http.Transport{Proxy: http.ProxyURL(proxy)}, Timeout: 30 * time.Second,}req, _ := http.NewRequest(http.MethodGet, site.String(), nil)req.Header.Set("User-Agent", task.Solution.UserAgent)resp, err := client.Do(req)<?php$curl = curl_init('https://shop.example.com/');curl_setopt_array($curl, [ CURLOPT_PROXY => getenv('PROXY_URL'), CURLOPT_USERAGENT => $userAgent, CURLOPT_COOKIE => 'cf_clearance=' . $clearance, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30,]);$page = curl_exec($curl);echo curl_getinfo($curl, CURLINFO_RESPONSE_CODE), PHP_EOL;A client that impersonates the browser’s TLS handshake, such as curl-impersonate, keeps the clearance accepted where plain HTTP libraries may be challenged again. When the site challenges you again, its clearance has ended: create a new task.
In a browser you drive, set the cookie for the site’s domain and the browser’s user agent to
solution.userAgent before loading the page, and route the browser through the same proxy. See
Browser automation.
Test against a live challenge page
Section titled “Test against a live challenge page”The Cloudflare WAF managed challenge test page, the Cloudflare 5-second JS challenge test page and the Cloudflare WAF interactive challenge test page each sit behind a Cloudflare WAF rule of that kind, and show whether a request carried a clearance. Point a challenge task at one to see the whole flow, then load the page with its clearance. Every test page is on the Cloudflare Turnstile demo and CAPTCHA test pages.
When it fails
Section titled “When it fails”A challenge that is not passed after every attempt fails with ERROR_CAPTCHA_UNSOLVABLE, and one
still unsolved at its deadline expires with ERROR_TASK_TIMEOUT; neither is charged. A proxy that
resolves to a private address by the time the task runs fails with ERROR_PROXY_NOT_ALLOWED. See
error codes for every code.