Inside the Cloudflare challenge platform: anatomy of the cf-chl orchestration
You have seen the page. A near-empty interstitial, a spinning indicator, the line about checking your connection, and then either the site loads or it does not. The whole thing usually clears in about a second. What happens in that second is a small, self-contained program that Cloudflare ships to your browser, runs once, and throws away. It probes the environment, does a little arithmetic the server can verify, posts the result back, and on success hands you a cookie that proves you passed. The interesting part is not that this happens. The interesting part is how tightly the page is wired so that the program is hard to lift out and replay.
This post takes the interstitial apart from the client side. We will follow the window._cf_chl_opt object that the page hands the script, the /cdn-cgi/challenge-platform/h/ orchestration endpoints the script calls, the obfuscation that keeps the payload from being read at rest, and the cf_clearance cookie that comes back when the answer checks out. Where Cloudflare documents a thing, the docs are cited. Where the detail lives only in observed traffic and third-party research, that is flagged plainly, because the exact byte layout of these payloads is not public and guessing at it is how reference posts get things wrong.
A note on scope before we start. This is a defensive teardown. It explains the machine so engineers building, debugging, or detecting against it understand what they are looking at. It does not hand you a working solver, and the parts where a solver would actually live are exactly the parts Cloudflare keeps opaque on purpose.
The platform, not the page
The first thing to get right is that “the Cloudflare challenge” is not one product. It is a shared engine that several Cloudflare features sit on top of. The WAF reaches for it when a custom rule or rate limit fires. Bot Fight Mode and Super Bot Fight Mode reach for it. Under Attack Mode reaches for it. Turnstile, the embeddable widget, is the same engine wearing a different shell. Cloudflare’s own challenges documentation is explicit that all of these route through one challenge platform, and that the interstitial Challenge Page and the Turnstile widget are two front-ends to the same client-side machinery.
That matters for anyone studying the interstitial, because the techniques and the endpoint shapes you see on a full-page challenge are the same ones Turnstile uses inline. If you have read our Turnstile internals teardown, most of the vocabulary here will be familiar. The interstitial is just the version that owns the whole viewport and blocks navigation until it resolves.
Cloudflare’s public story for the modern version of this began on April 1, 2022, when Reid Tatoris and Benedikt Wolters announced the Managed Challenge in a post titled The end of the road for Cloudflare CAPTCHAs. The framing of that post is worth holding onto: the goal was to stop showing visual puzzles. Instead of a grid of crosswalks, the platform would run a set of small non-interactive tests in the browser, weigh the signals, and only escalate to something a human has to click if the signals were inconclusive. Cloudflare reported the change cut average challenge time from 32 seconds to roughly one, dropped CAPTCHA usage by 91 percent over the following year, and lowered visitor abandonment by 31 percent against the old CAPTCHA path. Those are the numbers from the announcement, and they explain the design pressure: the platform is built to resolve invisibly for the overwhelming majority and to keep a visible puzzle in reserve for the rest.
The non-interactive tests are described, in Cloudflare’s own words, as proof-of-work, proof-of-space, probing for web APIs, and various checks for browser quirks and human behavior. The Turnstile how-it-works material lists the same set. Proof-of-work is a computational puzzle the browser must grind through; proof-of-space trades CPU for memory; the API probing pokes at native browser objects to see whether they behave the way a real engine behaves. None of these prove you are human on their own. Together they produce a vector of signals that the server scores. The 1-to-99 bot score that comes out of the broader pipeline is a separate topic, covered in our bot management scoring post; the challenge platform is the part that gathers the client-side evidence that feeds it.
The handshake: cdn-cgi and the orchestration endpoints
Every Cloudflare zone gets a reserved path prefix, /cdn-cgi/, that the edge serves directly and the origin never sees. Cloudflare’s reference on the endpoint lists the family: /cdn-cgi/trace for diagnostics, /cdn-cgi/image/ for image resizing, /cdn-cgi/l/email-protection for email obfuscation, /cdn-cgi/rum for analytics, and the one we care about, /cdn-cgi/challenge-platform/. That last prefix cannot be modified or customized by the site owner. It is Cloudflare’s, served by Cloudflare, on your domain.
When a request gets challenged, the edge does not return your page. It returns an interstitial HTML document with a 403 status, a tiny inline configuration object, and a script tag pointing into the challenge-platform path. From there the client script drives a multi-step conversation back to the edge. The shape of that conversation, reconstructed from observed traffic in the archived cfresearch repository and corroborated by several independent write-ups, runs roughly as follows.
*The challenge handshake. Orange marks the two moments the edge hands the browser something durable: the interstitial config on the way in, and the cf_clearance cookie on the way out. Everything between is the client proving it can run the script.*The first call after the page loads is a GET to a base under /cdn-cgi/challenge-platform/h/, with a sub-path along the lines of orchestrate/chl_page/v1?ray=<rayid>. The h/ segment is followed by a single letter, observed as g or b, which selects between two builds of the platform. The response is not data. It is JavaScript, the orchestration code that knows how to assemble the actual challenge, generate the challenge identifier, and decide which probes to run. The browser executes it, the challenge runs, and a POST goes to a flow/ endpoint carrying the encoded result. That flow URL has a long opaque structure, roughly flow/ov1/<token>/<ray>/<challenge-id>, and the request includes a cf-challenge header. When the edge is satisfied, its response carries a Cf-Chl-Gen header, the page reloads, and the reload comes back 200 with a Set-Cookie: cf_clearance. The exact contents of the flow/ token and the field layout of the POST body are not documented by Cloudflare; what is described here is the observed sequence, not a spec.
Worth noting: the split between the older interstitial style with orchestrate/chl_page and the newer inline JavaScript-Detections style is real and visible in the path. The lightweight detections flow loads its script from /cdn-cgi/challenge-platform/scripts/jsd/main.js and posts to a jsd/oneshot endpoint, which we get to below. The full interstitial is the heavier sibling that blocks the page.
The g versus b letter after h/ is the part people fixate on, and the honest answer is that Cloudflare has never said what the two builds differ on. Both have been seen serving working challenges in the same week, sometimes against the same zone, so it is not a simple old-versus-new toggle. The likeliest reading is that they are parallel platform variants Cloudflare can route between for staging or for traffic segmentation, and the letter is just which one your request landed on. Anyone hard-coding a path against one letter will eventually meet the other, which is a small but real reason the published bypass tools break on a schedule. The path is descriptive of where you are, not a contract about where you will be next time.
There is also a third party to the handshake that the sequence diagram leaves implicit: the interactive widget itself, when the platform decides it needs one. If the silent probes come back inconclusive, the orchestration can pull in a Turnstile-style widget served from challenges.cloudflare.com rather than from the zone’s own /cdn-cgi/ path. That cross-origin hop is why the interactive challenge can render even when the zone serving the page is locked down, and it is the seam where the interstitial and the embeddable widget visibly become the same product.
window._cf_chl_opt, the configuration the page hands the script
The inline object the interstitial sets is window._cf_chl_opt. It is the bridge between the static HTML and the dynamic script: the HTML cannot carry logic, so it carries parameters, and the script reads them. Cloudflare references this object directly in its custom challenge page docs, with a warning that site owners must not define their own window._cf_chl_opt, since any existing definition will collide with the platform’s. The one field Cloudflare names there is cTplC, which it sets to 1 when a custom challenge template is in use.
Beyond that single documented field, the object’s contents are known only from observation. Several third-party projects that track the challenge format, including the cloudscraper issue thread on the modern format and the cfresearch notes, converge on a recurring set of keys. The names below are reported by those projects, not by Cloudflare, and the meanings are inferred from how the script uses them. Treat the table as a field map drawn from traffic, not as documentation.
*Reported fields of window._cf_chl_opt. Orange marks the three that carry the most weight: cvId pins the script version, cRay is reused as a key, and cTplC is the one field Cloudflare actually documents. Everything else is inferred from observed pages and may change between builds.*Two of these deserve a closer look. cType selects the challenge flavor. The platform supports a managed mode that picks its own difficulty, a non-interactive mode that runs silently, and an interactive mode that puts a clickable widget in front of the user. The distinction between these maps onto the broader managed-versus-JS-versus-interactive split we cover in the challenge types post. The other is cRay. The Ray ID is normally just a request identifier you can read off the cf-ray response header, but in the challenge it does double duty: the orchestration script uses cRay as input to the routine that decrypts the next stage. That makes the whole sequence ray-bound. A payload captured under one Ray ID will not decrypt or validate under another, which is precisely the point.
Why you cannot just read the script
If you fetch the orchestration JavaScript and open it, you will not get far reading it top to bottom. It is machine-generated, heavily obfuscated, and rebuilt often. The two techniques that do the most work are string-array rotation and control-flow flattening, both confirmed in the 2026 architecture write-up by Ayush Aggarwal and visible in any captured sample.
String-array rotation pulls every literal in the program, every property name, every magic constant, out of the code and into one array, then rotates that array by some offset computed at load time. Each use site references a string by index into the rotated array rather than by its value. Read statically, the code is a sea of arr[0x1f3] lookups with no obvious meaning. You have to run the de-rotation to recover what any given index points at, and the de-rotation routine is itself buried in the same indirection. Control-flow flattening does the complementary job on the logic. Instead of functions with readable branching, the code becomes a single dispatch loop driven by a state variable: each iteration reads the state, runs one fragment, and computes the next state. The natural shape of the program is gone, replaced by a switch that hops around an opaque order. A decompiler can recover syntax but not intent.
On top of the static defenses there is a runtime one. The actual challenge logic does not sit in plain functions. It arrives as an encrypted string and is turned into executable code only at the moment it runs, by decrypting it with a routine keyed in part on cRay and then handing the result to the Function constructor. The decrypt-then-Function pattern means the interesting code never exists as readable source on disk or in the served file; it materializes for one execution and is bound to the Ray ID of that one challenge. The function name researchers commonly attach to the decrypt step is ax, observed across multiple captures, though the name is an artifact of a particular build and not a stable identifier.
There is one more moving part that turns the whole thing from a hard read into a maintenance treadmill for anyone trying to replicate it. The keys rotate. Reporting on the platform describes the encryption keys cycling on a roughly 30-minute cadence, which means a routine reverse-engineered against today’s build is stale within the hour. Whether the exact interval is 30 minutes everywhere is not something Cloudflare publishes, so take the number as the widely observed figure rather than a guaranteed constant. The effect is what counts: the platform is not a lock you pick once. It is a lock that re-keys itself while you are still working on it.
The lightweight cousin: JavaScript Detections and the oneshot
Not every Cloudflare challenge stops the page. The lighter path is JavaScript Detections, which Cloudflare describes in its JS Detections docs as an invisible snippet injected into HTML responses that runs without pausing the visitor. It is injected only for HTML page views, not for AJAX calls, and it loads from the same /cdn-cgi/challenge-platform/ prefix, specifically from a script such as scripts/jsd/main.js, with a manual-integration entry point at scripts/jsd/api.js.
The page bootstraps this with another inline object, distinct from _cf_chl_opt: window['__CF$cv$params'], which carries at minimum an r value (derived from the Ray ID) and a t value (a base64 timestamp). The script reads those, collects its signals, compresses the result, and posts it to a jsd/oneshot endpoint under the platform path. Reports of the wire format describe a body of the form v_<rayID>=<payload>, with the payload compressed and base64-encoded; sources disagree on whether the compression is zlib, gzip, or Brotli, which most likely reflects different builds rather than one stable answer. The honest statement is that it is compressed-then-encoded binary, and the precise codec is build-dependent and not documented.
The signals themselves are the usual browser-fingerprinting surface: screen resolution and color depth, timezone offset, the WebGL renderer string, an audio-context fingerprint, a canvas-draw hash, plugin and navigator properties. These overlap heavily with what every fingerprinting library collects, and the TLS and HTTP/2 fingerprinting the edge does in parallel adds a network-layer dimension the JavaScript cannot fake. The point of the JavaScript half is not any single signal. It is the consistency check: a real Chrome reports a coherent set of values, and a headless or instrumented environment tends to report a set that disagrees with itself.
What comes back from a successful detection is not a separate token. It is folded into the cf_clearance cookie, which Cloudflare says carries the outcome in a field expressed as cf.bot_management.js_detection.passed, set to true on a pass. A subtlety the docs are careful about: a failed detection does not block anything on its own. The site owner has to write a WAF rule that references that field. The detection populates a signal; enforcement is a separate decision. Cloudflare puts the lifespan of the detection result at 15 minutes.
The pass: cf_clearance and what it actually asserts
When the interstitial challenge succeeds, the edge issues cf_clearance via Set-Cookie, and that cookie is the artifact the rest of the session leans on. We have a whole post on the cookie’s issuance, scope, and lifetime; here the relevant point is narrower. The cookie is the receipt for a passed challenge, and its value is opaque, signed server-side, and bound to attributes the edge can re-check on every subsequent request.
The binding is the part that matters for understanding the orchestration. A cf_clearance cookie is not a bearer token you can lift and use anywhere. It is tied to context the edge captured at issue time, including the network and transport fingerprint of the client that earned it. Reported lifetimes vary from 30 minutes to several hours depending on the zone’s configuration, and Turnstile’s pre-clearance mode can issue a longer-lived cf_clearance after a verification on an initial HTML page, which then lets the visitor reach guarded API endpoints without re-challenging. The cookie’s exact internal layout is not published, so what it encodes is described here from behavior, not from a documented schema.
This is where the whole design closes the loop. The challenge platform is hard to read at rest because of obfuscation, hard to lift because it is keyed on the Ray ID, hard to maintain against because the keys rotate, and hard to replay because the proof it produces is bound to the client that produced it. Each defense covers a different attack. Obfuscation slows the person reading the script. Ray-binding stops the captured payload from being reused. Rotation breaks the cached solver. Cookie-binding stops the earned cookie from being shared. You can defeat any one of these in isolation. Defeating all four at once, continuously, while the keys move under you, is the actual cost.
What the architecture tells you
Read as a system rather than a list of tricks, the cf-chl platform is a study in making a client-side check expensive to forge without making it expensive to pass. The honest visitor pays a second of CPU once and never thinks about it again. The party trying to replicate the flow pays an ongoing engineering bill, because every layer is designed to expire: the script is regenerated, the keys rotate, the payload is single-use, and the cookie is non-transferable. There is no single secret to extract. There is a moving target to chase.
The other thing the teardown makes plain is how little of this is actually documented, and how deliberate that gap is. Cloudflare publishes the endpoint prefix, the existence of cf_clearance, the cf.bot_management.js_detection.passed field, the one config key cTplC, and the high-level menu of proof-of-work and proof-of-space and API probing. It does not publish the _cf_chl_opt field layout, the flow-token structure, the decryption routine, the payload codec, or the key schedule. Those are exactly the details a solver needs and exactly the details that stay in the dark. Everything specific in this post that touches them is sourced to observed traffic and third-party research and labeled as such, because the alternative, inventing a clean field table and presenting it as fact, is how a reference post becomes wrong the next time Cloudflare ships a build. The platform’s best defense was never any one mechanism. It is that the mechanisms keep moving, and the parts worth knowing are the parts nobody outside Cloudflare gets to write down with certainty.
Sources & further reading
- Tatoris, R. and Wolters, B. (2022), The end of the road for Cloudflare CAPTCHAs — the Managed Challenge announcement, with the 32-second-to-1-second and 91-percent figures and the proof-of-work / proof-of-space description.
- Cloudflare (2026), Cloudflare challenges: overview — which products issue challenges and how Challenge Pages and Turnstile share one platform.
- Cloudflare (2026), How challenges work — the non-interactive test menu and the cf_clearance / js_detection.passed signal.
- Cloudflare (2026), JavaScript Detections — the injected snippet, the scripts/jsd paths, the 15-minute lifespan, and the cf.bot_management.js_detection.passed field.
- Cloudflare (2026), Challenge pages: additional configuration — the documented cTplC field, the custom-template placeholders, and the do-not-redefine-_cf_chl_opt warning.
- Cloudflare (2026), The /cdn-cgi/ endpoint — the reserved path family including /cdn-cgi/challenge-platform/.
- Cloudflare (2026), Turnstile — the same proof-of-work / proof-of-space / API-probing menu, in the widget context.
- scaredos (2025, archived), cfresearch — observed orchestrate/chl_page and flow endpoint shapes, the cf-challenge and Cf-Chl-Gen headers, and the request sequence.
- VeNoMouS (2025), cloudscraper issue #298: modern challenge format — community-reported _cf_chl_opt field names for the current format and the JSON-payload submission shift.
- Aggarwal, A. (2026), Advanced evasion techniques and architecture analysis of Cloudflare Bot Management — string-array rotation, control-flow flattening, the telemetry surface, and the jsd/oneshot endpoint.
Further reading
Inside the DataDome JS tag: what ddjskey and the client payload carry
A reference on DataDome's client-side JavaScript tag: the ddjskey site identifier, the signals the browser collector gathers and posts to api-js.datadome.co, and how the challenge and interstitial flow is wired.
·21 min readKasada's KPSDK: the 128-bit token, the VM, and the obfuscated bytecode
Traces how Kasada's client SDK works: the x-kpsdk-ct and x-kpsdk-cd tokens, the obfuscated JavaScript VM that runs Kasada-specific bytecode, the proof-of-work it computes, and how the payload rotates per tenant.
·19 min readDeobfuscating anti-bot JavaScript: the workflow for VM-based protections
How to read obfuscated anti-bot JavaScript without running it blind: beautify, scope and string-array recovery, Babel AST transforms, runtime hooking, and where the workflow hits a wall against bytecode VMs.
·20 min read