Skip to content

Tutorial

Playwright and Cloudflare "Just a Moment": Using cf_clearance

Why Playwright gets stuck on Cloudflare's "Just a moment..." page, how to detect it, and how to load a cf_clearance cookie with its user agent and proxy.

By 4 min readPublished Updated

When Playwright opens a site and stays on “Just a moment…”, it has met a Cloudflare challenge page: a rule on the site asks every visitor to pass Cloudflare’s check before the page loads. Cloudflare documents that browser automation frameworks, “such as Selenium, Puppeteer, Playwright, and Cypress, are not supported for solving production challenges”, so waiting often does not help. What the page wants is a cf_clearance cookie, which a browser earns by passing the challenge. In Playwright you can use one: create a context with the cookie’s user agent, add the cookie, and browse through the same proxy that earned it. This tutorial shows how to detect the challenge and load a clearance, on sites you are allowed to automate.

Detect the challenge

Cloudflare’s docs say a challenge response carries the header cf-mitigated: challenge, whatever resource was requested. Checking it is more reliable than looking for the title:

import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
const response = await page.goto("https://shop.example.com/", { waitUntil: "domcontentloaded" });
if (response?.headers()["cf-mitigated"] === "challenge") {
console.log(`Cloudflare challenge, HTTP ${response.status()}: ${await page.title()}`);
}
await browser.close();

A challenge usually answers with HTTP 403 and the title “Just a moment…”. An error page with a code such as 1020 is different: a rule blocked the request, and no cookie helps. See Cloudflare error 1020.

Why the clearance needs its user agent and proxy

Passing a challenge sets cf_clearance, which Cloudflare says “proves to Cloudflare that the visitor is a verified human” and “is securely tied to the specific visitor and device it was issued to”. Its lifetime is the site’s Challenge Passage setting, 30 minutes by default. In practice that means three things for Playwright:

  1. The same user agent. Set the context’s userAgent to exactly the one the cookie was issued with.
  2. The same network. Cloudflare notes that a Managed Challenge solved from a different IP than the one it was issued to is not valid, so browse through the proxy that earned the clearance.
  3. A browser that matches the user agent. A user agent string says which browser it is; Chromium’s own behavior should agree. Use a Chromium whose major version is close to the one in the user agent. A mismatch is a signal Cloudflare may act on, and it will challenge again.

Get a clearance and load it

ZeroCaptcha’s challenge task passes the page through your proxy and returns the cf_clearance cookie with the user agent it was issued for. The task takes the page’s URL and your proxy; a challenge page has no sitekey. The compatible format answers the way CapSolver’s AntiCloudflareTask does, with solution.userAgent and solution.cookies.cf_clearance. The challenge pages docs list every field.

Save this as clearance.mjs, with playwright installed and ZEROCAPTCHA_API, ZEROCAPTCHA_KEY and PROXY_URL (such as http://user:pass@proxy.example.net:8080) set:

import { chromium } from "playwright";
const API = process.env.ZEROCAPTCHA_API;
const KEY = process.env.ZEROCAPTCHA_KEY;
const PROXY_URL = process.env.PROXY_URL;
const TARGET = "https://shop.example.com/";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(method, body) {
const response = await fetch(`${API}/${method}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ clientKey: KEY, ...body }),
signal: AbortSignal.timeout(15_000),
});
const reply = await response.json();
if (reply.errorId) throw new Error(`${method}: ${reply.errorCode}`);
return reply;
}
async function getClearance(websiteURL, proxy) {
const { taskId } = await call("createTask", { task: { type: "CloudflareChallengeTask", websiteURL, proxy } });
const deadline = Date.now() + 180_000;
while (Date.now() < deadline) {
await sleep(2_000);
const result = await call("getTaskResult", { taskId });
if (result.status === "ready") {
return { userAgent: result.solution.userAgent, cookie: result.solution.cookies.cf_clearance };
}
}
throw new Error(`task ${taskId}: no clearance within 180 seconds`);
}
const { userAgent, cookie } = await getClearance(TARGET, PROXY_URL);
// Browse through the same proxy, with the same user agent, carrying the cookie.
const proxy = new URL(PROXY_URL);
const browser = await chromium.launch({
proxy: {
server: `${proxy.protocol}//${proxy.host}`,
username: decodeURIComponent(proxy.username),
password: decodeURIComponent(proxy.password),
},
});
const context = await browser.newContext({ userAgent });
await context.addCookies([{ name: "cf_clearance", value: cookie, url: TARGET }]);
const page = await context.newPage();
const response = await page.goto(TARGET, { waitUntil: "domcontentloaded" });
console.log(response?.headers()["cf-mitigated"] === "challenge" ? "Challenged again" : await page.title());
await browser.close();

Run it with node clearance.mjs. A challenge that is not passed ends with ERROR_CAPTCHA_UNSOLVABLE, and a proxy that resolves to a private address with ERROR_PROXY_NOT_ALLOWED; neither costs anything. The errors reference lists every code.

When the challenge comes back

  • After about 30 minutes: the site’s Challenge Passage ended. Get a new clearance.
  • At once: the cookie is being sent from another IP, with another user agent, or by a browser whose other signals contradict the user agent. Check the proxy and the Chromium version.
  • On some requests only: a CORS preflight (OPTIONS) does not carry the cookie, and Cloudflare lists rules features combined with challenges among the causes of loops. See Cloudflare challenge loop.

The cf_clearance cookie explained covers the cookie itself, and the Cloudflare WAF and 5-second challenge solver page the task type. For a Cloudflare Turnstile widget inside a form, which needs a token rather than a cookie, see Playwright on the Cloudflare Turnstile solver page. Automate only sites you are allowed to: see responsible captcha automation.

ZeroCaptcha is not affiliated with CapSolver; its name appears only to describe the reply format.

Sources

The team that builds and runs the ZeroCaptcha API. Articles are drafted with AI tools, then checked against the API's code and the primary sources each one cites.

Questions

Why does Playwright get stuck on Cloudflare's "Just a moment..." page?

Cloudflare documents that browser automation frameworks, Playwright included, are not supported for solving production challenges, so an automated browser may never pass the page. The page is a Cloudflare challenge that wants a cf_clearance cookie.

How do I use a cf_clearance cookie in Playwright?

Create a browser context with the user agent the cookie was issued for, add the cookie with context.addCookies, and browse through the same proxy that earned it. Cloudflare ties the cookie to the visitor and device it was issued to.

How do I detect a Cloudflare challenge in Playwright?

Check the navigation response: a challenge page is served with a cf-mitigated header set to challenge, usually with HTTP 403 and the title "Just a moment...".

Read next

This article is part of the Cloudflare WAF and 5-second challenge solver hub. Every task is charged only when a token is ready.

Get an API key