Cloudflare "Just a Moment" in Python: Why requests Fails

Cloudflare's 'Just a moment...' page is a JavaScript challenge, and Python requests cannot run JavaScript. Here is how to detect it, why it hits servers harder than laptops, why cloudscraper-style tricks stopped being enough, and what got through in our 29 September 2026 test.

'Just a moment...' is Cloudflare's challenge page. It asks the client to run JavaScript and prove it is a browser before the site answers. Python requests cannot run JavaScript, and from a datacenter server its IP and TLS handshake already look automated. What gets through is a real browser on a clean IP, or a web unlocker that runs one.

This guide shows how to detect the challenge in Python, why the same script passes on your laptop and fails on your server, why cloudscraper-era tricks stopped being enough, and what actually got through when we tested it on 29 September 2026. That includes the site where nothing we tried got past 'Just a moment'.

What the "Just a moment..." page actually is

Cloudflare calls it an interstitial Challenge Page: a full-page gate between the visitor and the site while Cloudflare decides whether the visitor is automated. Its documentation on challenge pages describes three kinds. A non-interactive challenge only needs the browser to run the injected JavaScript. A managed challenge lets Cloudflare pick the challenge type from the request's signals. An interactive challenge asks the visitor to do something. When the check passes, the original request continues to the site and the browser gets a cf_clearance cookie that lets it skip the gate for a while.

A Python HTTP client never gets that far. It downloads the challenge page, has no engine to run the script, and hands you the HTML of the gate instead of the page you asked for.

Why the challenge comes back as a 403, and how to detect it

In our test, every challenge page arrived with status 403, which is why raise_for_status() throws 403 Client Error: Forbidden and people search for 'cloudflare just a moment 403'. The status alone cannot tell a challenge from any other refusal, though. The reliable signal is a header: Cloudflare documents that a challenged response carries cf-mitigated: challenge, whatever resource you requested. Check that first, then the title as a fallback:

import re
import requests

URL = "https://www.glassdoor.com/Reviews/Google-Reviews-E9079.htm"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}


def title_of(html: str) -> str:
    m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else ""


def is_cloudflare_challenge(r: requests.Response) -> bool:
    if r.headers.get("cf-mitigated") == "challenge":      # documented by Cloudflare
        return True
    if "just a moment" in title_of(r.text).lower():
        return True
    return "enable javascript and cookies to continue" in r.text[:20000].lower()


r = requests.get(URL, headers=HEADERS, timeout=30)
print(r.status_code, is_cloudflare_challenge(r), title_of(r.text))
# 29 Sep 2026, plain request through a residential proxy (US exit):
# 403, body asking to 'Enable JavaScript and cookies to continue'

Do not retry a challenged request in a tight loop with the same client. Each retry fetches the same gate, and some sites answer repeated failures with stricter rules. That is how scrapers end up stuck in a 'Just a moment' loop.

"Enable JavaScript and cookies to continue" and "your browser does not support JavaScript"

These strings are the fallback text of a challenge page, shown to clients that do not execute its script. When Python requests shows you one of them, the site is not complaining about your headers. It is telling you, literally, that it needs a JavaScript engine and a cookie jar that keeps what the script sets.

That is exactly what our plain residential run received from Indeed and Glassdoor: a 403 with 'Enable JavaScript and cookies to continue', from a clean home IP. Adding a Chrome User-Agent, an Accept-Language header or a Referer changes nothing, because the page is not reading them to decide. It is waiting for the script to run.

Why your scraper works locally but gets blocked by Cloudflare on a server

This is the most common version of the complaint: the script works on a laptop, then gets 'Just a moment' as soon as it runs on a cloud VM. Two things change between those machines.

The residential column of our test shows the limit of fixing only the IP. Zillow and Leboncoin served the real page to a plain request through a residential proxy. Indeed and Glassdoor still answered with the JavaScript challenge. Your laptop can hit the same wall on a stricter site. It simply hits it less often.

Why a Python scraper passes Cloudflare from a laptop and gets the Just a moment challenge from a cloud server: home IP versus hosting IP, same Python TLS fingerprint, no JavaScript engine in either case
Same script, two machines. The IP changes; the Python TLS handshake and the missing JavaScript engine do not.

Why cloudscraper-era tricks stop working

cloudscraper was built for Cloudflare's older 'I'm Under Attack' page. That page mostly checked whether the client could run a JavaScript computation, so the library fetched the challenge, solved the script in an embedded interpreter, sent the answer back and kept the cookie, all on top of requests. For a long time that was enough, and it is the reason 'cloudscraper alternative' is now a search people make.

Several things have moved since then, and they do not depend on any one library version:

As of 29 September 2026, the cloudscraper repository's last push was in June 2025. That is not a verdict on any particular site, and we did not benchmark it for this article. It does mean you should test it against your own targets rather than assume it works. The alternatives each fix part of the problem. curl_cffi reproduces a browser TLS handshake but runs no JavaScript. Playwright or another real browser runs the challenge but still needs a clean IP, and is heavy to operate at volume. The unlocker combines the two and tells you when neither worked.

What gets through: the web unlocker path

The QuantumProxies.io Web Unlocker handles a GET in stages. First, up to three TLS attempts, each on a fresh residential exit under a different browser fingerprint. If every attempt comes back challenged, it escalates to a headless browser that runs the challenge. If the page is still blocked after that, you get an error status with x-qp-unlocker-blocked and a block class, never a challenge page dressed up as a 200. From Python it is a proxy setting:

import os
import time
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

user = os.environ["QP_UNLOCK_USER"] + "-country-us"          # unlock-XXXXXXXX-country-us
PROXY = f"http://{user}:{os.environ['QP_UNLOCK_PASSWORD']}@unlock.quantumproxies.io:9000"

s = requests.Session()
s.proxies = {"http": PROXY, "https": PROXY}
s.verify = False                     # or the unlocker CA file from /api/v1/scraper/unlock/ca
s.headers["x-qp-session"] = "glassdoor-01"   # same value on every call: same exit, reusable clearance

for attempt in (1, 2):               # same page twice here; in practice, the next page
    t0 = time.monotonic()
    r = s.get(URL, timeout=120)
    print(attempt, r.status_code, f"{time.monotonic() - t0:.1f}s",
          "rendered:", r.headers.get("x-qp-unlocker-rendered"),
          "clearance:", r.headers.get("x-qp-clearance"),
          "block:", r.headers.get("x-qp-block-class"))

The x-qp-* headers are read by the unlocker and never forwarded to the site. If you would rather not deal with the certificate at all, the REST endpoint POST https://app.quantumproxies.io/api/v1/scraper/unlock takes the URL in a JSON body with your API key and returns blocked, blockClass and vendor in the response. Our web unlocker API tutorial walks through both.

Measured: two Cloudflare-style walls that passed

Measured on 29 September 2026. Plain requests were curl with a Chrome User-Agent, from a Frankfurt datacenter server and through a residential proxy with a US exit. The unlocker used the residential tier with a US exit. Times are wall-clock for one request.

SitePlain, datacenter serverPlain, residential proxyWeb Unlocker
Glassdoor (company reviews)403, 'Security' page403, 'Enable JavaScript and cookies to continue'200, 2 attempts + headless browser, 17.2 s
Indeed (job search)403, 'Security Check' page403, 'Enable JavaScript and cookies to continue'200, 1 attempt, no browser, 2.6 s

Glassdoor is the full path. Two TLS attempts were challenged, then the headless browser ran the challenge and returned the reviews page. Indeed is the interesting one: the challenge that the plain residential request received never appeared for the unlocker's first attempt. A browser TLS fingerprint on a fresh residential exit was enough, without a browser.

Get past JavaScript challenges with the Web Unlocker

Reusing the clearance: why the second request is fast

The browser escalation is the slow part, and you do not want to pay for it on every page. When the headless browser passes a challenge, it earns a clearance. Send the same x-qp-session value, or keep the same -session-<id> in the proxy username, on the following requests and they leave from the same exit, reusing that clearance instead of running the browser again. The x-qp-clearance header tells you what happened on each call: minted when a new clearance was earned, hit when a stored one was reused, miss when there was nothing to reuse.

In a Glassdoor test on 29 September 2026, with the session pinned in the proxy username and two separate connections, the first request went through the browser in 14.0 seconds (minted) and the second came back in 1.4 seconds (hit). The pinning matters because of how Cloudflare binds clearance: it is tied to the visitor and device that earned it, and it stays valid for the duration the site configures unless Cloudflare decides the session looks suspicious. Change the exit or the fingerprint and you are a new visitor again.

Timeline of two requests in one pinned unlocker session: the first escalates to a headless browser and mints a Cloudflare clearance in 14.0 seconds, the second reuses it and returns in 1.4 seconds
First request pays for the browser; later requests in the same pinned session reuse the clearance.

When plain requests is still fine on a Cloudflare site

Plenty of sites run behind Cloudflare only as a CDN, or with bot rules that let ordinary traffic through, and plain requests works on them. Being 'on Cloudflare' is not a reason to reach for a browser. The signal is the response. If there is no cf-mitigated: challenge header, no 'Just a moment' title and no 403, keep the plain request, and escalate only the URLs that actually get challenged. The plain-first helper in our tutorial does exactly that.

Two neighbouring problems have their own guides. A page that says 'Error 1020 Access denied' is a firewall rule refusing you, not a challenge, and no amount of JavaScript fixes it; see what triggers Cloudflare error 1020 and what fixes it. For the wider picture of Cloudflare's defences beyond Python, read how to bypass Cloudflare challenges in 2026. If the page behind the wall is Glassdoor, our guide to scraping Glassdoor salaries and reviews covers the data side.

Frequently asked questions

Why does my scraper work locally but get blocked by Cloudflare in the cloud?

Because the IP changes. At home your script leaves from a residential ISP address; on a cloud VM it leaves from a hosting range that bot defences distrust on sight. The Python TLS handshake is the same on both, and on a stricter site it gets challenged even from home. In our test, a residential IP alone did not pass Indeed or Glassdoor.

Why does Cloudflare's "Just a moment" page return a 403?

The challenge page is served instead of the page you asked for, and in our test it came with status 403, so requests raises '403 Client Error: Forbidden'. The status is not specific to challenges. Check the cf-mitigated response header, which Cloudflare documents as set to challenge on every Challenge Page response, or the 'Just a moment' title.

Is cloudscraper still working, and what is a good alternative?

It may still work on sites with lenient settings, so test it against your targets. It runs on Python's TLS stack and solves scripts in an interpreter, while current challenges also look at the handshake and the browser environment. Alternatives: curl_cffi for a browser handshake, a real browser for the JavaScript, or a web unlocker that does both.

Can Python requests pass a Cloudflare challenge on its own?

No. requests does not execute JavaScript, so it cannot complete a challenge that depends on running the page's script. It can fetch pages from Cloudflare sites that are not challenging you. Once cf-mitigated: challenge appears, you need a real browser, directly or through a service that runs one, and ideally a clean residential IP.

How long does a Cloudflare clearance last?

As long as the site configures it to. Cloudflare calls the setting Challenge Passage, and clearance can end early if Cloudflare decides the session looks suspicious. The cf_clearance cookie is tied to the visitor and device that earned it, so it only helps if later requests keep the same exit and fingerprint, which is what pinning x-qp-session does.

The short version: 'Just a moment...' asks for a browser, so no header trick in requests will satisfy it. Detect it with cf-mitigated, keep plain requests for everything that is not challenged, send the challenged URLs through something that runs a real browser from a clean IP, and pin a session so you pay for that browser once rather than on every page.

Try the Web Unlocker on residential exits