How to Bypass Kasada Anti-Bot Protection

How to Bypass Kasada Anti-Bot Protection

Kasada is one of the most technically advanced anti-bot solutions available. it protects major brands in ticketing, ecommerce, financial services, and gaming. what makes Kasada different from competitors like DataDome or Cloudflare is its use of proof-of-work challenges and its deep integration into the client-server request cycle.

if you’re scraping a Kasada-protected site and getting blocked, this guide explains how the system works and what techniques can get around it.

How Kasada Works

Kasada’s detection system, branded as “Polyform,” operates differently from traditional WAFs. instead of simply checking headers and cookies, Kasada makes your browser prove it’s real by solving computational challenges.

Proof of Work (PoW) Challenges

this is Kasada’s signature feature. when you visit a protected page, Kasada’s JavaScript generates a proof-of-work puzzle that your browser must solve. this puzzle:

  • requires actual CPU computation
  • takes 50-500ms for a real browser to solve
  • generates a unique token that proves the work was done
  • is tied to your specific session and cannot be reused
  • changes with every request

Client-Side Integrity Checks

beyond proof of work, Kasada’s JavaScript performs extensive browser environment analysis:

  • JavaScript engine fingerprinting – checks for engine-specific behaviors that differ between real browsers and headless environments
  • DOM API consistency – verifies that DOM APIs behave exactly as they would in a genuine browser
  • WebGL and Canvas fingerprinting – generates hardware-dependent rendering hashes
  • timing analysis – measures how long operations take, since headless browsers and emulated environments have different timing profiles
  • navigator property inspection – deep inspection of browser properties beyond just webdriver

Server-Side Analysis

on the server side, Kasada analyzes:

  • TLS fingerprint (JA3/JA4) – compares the TLS handshake against known browser profiles
  • HTTP/2 fingerprint – analyzes HTTP/2 SETTINGS frames and HEADERS frame ordering
  • request timing – measures the time between the challenge being issued and the solution arriving
  • IP reputation – standard IP-based checks similar to other WAFs

Detecting Kasada Protection

from curl_cffi import requests

def detect_kasada(url):
    """check if a website uses Kasada protection"""
    session = requests.Session(impersonate="chrome124")
    response = session.get(url, allow_redirects=False)

    text = response.text
    headers_lower = {k.lower(): v for k, v in response.headers.items()}

    indicators = {
        "kasada_script": "/ips.js" in text or "ct.captcha-delivery" in text,
        "kasada_header": "x-kpsdk" in " ".join(headers_lower.keys()),
        "cd_cookie": any(
            "_abck" in c or "bm_" in c
            for c in response.cookies.keys()
        ),
        "polyform_js": "polyform" in text.lower(),
        "kasada_challenge": "429" == str(response.status_code)
            and "kasada" in text.lower(),
    }

    is_kasada = any(indicators.values())
    print(f"Kasada detected: {is_kasada}")
    for check, result in indicators.items():
        print(f"  {check}: {result}")

    return is_kasada

detect_kasada("https://example-site.com")

Why Kasada Is Harder to Bypass

compared to other anti-bot solutions, Kasada presents unique challenges:

  1. proof of work can’t be faked – you actually need to run the JavaScript and perform the computation
  2. tokens are single-use – each request requires a fresh proof-of-work token
  3. timing verification – if the PoW solution comes back too fast (suggesting a powerful server, not a browser) or too slow (suggesting an emulator), it gets flagged
  4. obfuscation updates frequently – Kasada regularly changes its JavaScript obfuscation, breaking any attempts to reverse-engineer the challenge logic

Method 1: Playwright with Stealth Patches

the most reliable approach for Kasada is using a real browser with extensive stealth modifications.

import asyncio
from playwright.async_api import async_playwright

STEALTH_SCRIPT = """
// remove webdriver flag
Object.defineProperty(navigator, 'webdriver', {get: () => undefined});

// fix chrome object
window.chrome = {
    runtime: {
        onMessage: {addListener: () => {}, removeListener: () => {}},
        sendMessage: () => {},
        connect: () => ({onMessage: {addListener: () => {}}, postMessage: () => {}}),
    },
    loadTimes: () => ({
        requestTime: Date.now() / 1000 - Math.random() * 2,
        startLoadTime: Date.now() / 1000 - Math.random(),
        firstPaintTime: Date.now() / 1000 - Math.random() * 0.5,
    }),
    csi: () => ({startE: Date.now(), onloadT: Date.now()}),
};

// fix permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
    parameters.name === 'notifications' ?
        Promise.resolve({state: Notification.permission}) :
        originalQuery(parameters)
);

// fix plugins
Object.defineProperty(navigator, 'plugins', {
    get: () => [
        {name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer'},
        {name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai'},
        {name: 'Native Client', filename: 'internal-nacl-plugin'},
    ],
});

// fix languages
Object.defineProperty(navigator, 'languages', {
    get: () => ['en-US', 'en'],
});
"""

async def scrape_kasada_site(url, proxy=None):
    async with async_playwright() as p:
        launch_options = {
            "headless": False,  # Kasada almost always blocks headless
            "args": [
                "--disable-blink-features=AutomationControlled",
                "--disable-dev-shm-usage",
                "--disable-infobars",
                "--window-size=1920,1080",
            ],
        }

        if proxy:
            launch_options["proxy"] = {
                "server": proxy["server"],
                "username": proxy.get("username"),
                "password": proxy.get("password"),
            }

        browser = await p.chromium.launch(**launch_options)
        context = await browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
            locale="en-US",
            timezone_id="America/New_York",
            color_scheme="light",
        )

        await context.add_init_script(STEALTH_SCRIPT)

        page = await context.new_page()

        # navigate and wait for Kasada's PoW to complete
        response = await page.goto(url, wait_until="networkidle")

        # Kasada's PoW typically resolves within 2-5 seconds
        await page.wait_for_timeout(5000)

        # check if we passed the challenge
        final_url = page.url
        content = await page.content()

        # extract cookies for potential reuse
        cookies = await context.cookies()

        await browser.close()

        return {
            "content": content,
            "url": final_url,
            "cookies": {c["name"]: c["value"] for c in cookies},
            "status": response.status if response else None,
        }

# usage
result = asyncio.run(scrape_kasada_site(
    "https://kasada-protected-site.com",
    proxy={
        "server": "http://residential-proxy:port",
        "username": "user",
        "password": "pass",
    }
))

print(f"final URL: {result['url']}")
print(f"content length: {len(result['content'])}")

you can verify your browser stealth setup using the Browser Fingerprint Tester on dataresearchtools.com before running against a live target.

Method 2: Persistent Browser Sessions

since Kasada’s PoW is expensive per request, keeping a browser session alive is more efficient than opening new ones.

import asyncio
from playwright.async_api import async_playwright
import time

class KasadaBrowserPool:
    def __init__(self, proxy, pool_size=3):
        self.proxy = proxy
        self.pool_size = pool_size
        self.browsers = []
        self.pages = []

    async def initialize(self):
        """start browser instances"""
        self.pw = await async_playwright().__aenter__()

        for i in range(self.pool_size):
            browser = await self.pw.chromium.launch(
                headless=False,
                args=["--disable-blink-features=AutomationControlled"],
                proxy={
                    "server": self.proxy["server"],
                    "username": self.proxy.get("username"),
                    "password": self.proxy.get("password"),
                }
            )

            context = await browser.new_context(
                viewport={"width": 1920, "height": 1080},
                user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
                locale="en-US",
                timezone_id="America/New_York",
            )

            await context.add_init_script(STEALTH_SCRIPT)
            page = await context.new_page()

            self.browsers.append(browser)
            self.pages.append(page)

        print(f"initialized {self.pool_size} browser instances")

    async def scrape(self, url, browser_idx=0):
        """scrape a URL using a specific browser instance"""
        page = self.pages[browser_idx % self.pool_size]

        await page.goto(url, wait_until="networkidle")
        await page.wait_for_timeout(3000)

        content = await page.content()
        return content

    async def scrape_urls(self, urls):
        """scrape multiple URLs distributing across browser instances"""
        results = []
        for i, url in enumerate(urls):
            browser_idx = i % self.pool_size
            content = await self.scrape(url, browser_idx)
            results.append({"url": url, "content": content})

            # delay between requests
            await asyncio.sleep(3 + (i % 3) * 2)

        return results

    async def close(self):
        """close all browser instances"""
        for browser in self.browsers:
            await browser.close()
        await self.pw.__aexit__(None, None, None)

# usage
async def main():
    pool = KasadaBrowserPool(
        proxy={
            "server": "http://residential-proxy:port",
            "username": "user",
            "password": "pass",
        },
        pool_size=3
    )

    await pool.initialize()

    results = await pool.scrape_urls([
        "https://kasada-site.com/page/1",
        "https://kasada-site.com/page/2",
        "https://kasada-site.com/page/3",
    ])

    for r in results:
        print(f"{r['url']}: {len(r['content'])} bytes")

    await pool.close()

asyncio.run(main())

Method 3: Intercepting API Calls

for sites where the data loads via API calls after the initial page loads, you can intercept the API responses directly from the browser.

import asyncio
from playwright.async_api import async_playwright
import json

async def intercept_kasada_api(url, api_pattern):
    """intercept API calls made after Kasada's PoW challenge passes"""
    api_responses = []

    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=False,
            args=["--disable-blink-features=AutomationControlled"],
        )

        context = await browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        )

        await context.add_init_script(STEALTH_SCRIPT)
        page = await context.new_page()

        # listen for API responses
        async def handle_response(response):
            if api_pattern in response.url:
                try:
                    body = await response.json()
                    api_responses.append({
                        "url": response.url,
                        "status": response.status,
                        "data": body,
                    })
                    print(f"captured API response: {response.url}")
                except Exception:
                    pass

        page.on("response", handle_response)

        # navigate and wait for the page (and API calls) to load
        await page.goto(url, wait_until="networkidle")
        await page.wait_for_timeout(5000)

        await browser.close()

    return api_responses

# usage: capture product API data from a Kasada-protected ecommerce site
api_data = asyncio.run(intercept_kasada_api(
    "https://kasada-protected-store.com/products",
    api_pattern="/api/v1/products"
))

for response in api_data:
    print(f"URL: {response['url']}")
    print(f"items: {len(response['data'].get('items', []))}")

Proxy Requirements for Kasada

Kasada has the strictest IP requirements of any anti-bot solution. here’s the reality:

Proxy TypeWorks with Kasada?Notes
DatacenterAlmost NeverKasada blocks virtually all datacenter IP ranges
Shared ResidentialSometimes (~50%)depends on the residential pool quality
Premium ResidentialUsually (~75%)better pool with cleaner IPs
ISP/Static ResidentialGood (~80%)consistent IP helps with session tracking
Mobile 4G/5GBest (~90%+)highest trust, but most expensive

for Kasada specifically, mobile proxies provide the highest success rate. compare costs using the Proxy Cost Calculator since mobile proxy pricing varies significantly between providers.

Timing Considerations

Kasada performs timing analysis on PoW solutions. your approach needs to account for this:

import time
import random

class KasadaTimingManager:
    def __init__(self):
        self.min_page_time = 3  # minimum seconds to "view" a page
        self.max_page_time = 15

    def simulate_page_view(self):
        """simulate realistic page viewing time"""
        # real users spend variable time on pages
        view_time = random.gauss(mu=7, sigma=3)
        view_time = max(self.min_page_time, min(view_time, self.max_page_time))
        return view_time

    def simulate_navigation(self):
        """simulate the delay between clicking a link and loading"""
        # this accounts for "decision time" before clicking
        decision_time = random.uniform(0.5, 2.0)
        return decision_time

# usage in a scraping loop
timing = KasadaTimingManager()

async def scrape_with_timing(page, urls):
    results = []
    for url in urls:
        # simulate deciding to click
        await asyncio.sleep(timing.simulate_navigation())

        await page.goto(url, wait_until="networkidle")

        # simulate reading the page
        view_time = timing.simulate_page_view()
        await asyncio.sleep(view_time)

        content = await page.content()
        results.append(content)

    return results

Adding Mouse Movement and Scrolling

Kasada tracks interaction patterns. adding realistic mouse movements and scrolling increases your success rate.

import random

async def simulate_human_behavior(page):
    """simulate realistic human interactions on a page"""
    viewport = page.viewport_size

    # random mouse movements
    for _ in range(random.randint(2, 5)):
        x = random.randint(100, viewport["width"] - 100)
        y = random.randint(100, viewport["height"] - 100)
        await page.mouse.move(x, y, steps=random.randint(5, 15))
        await asyncio.sleep(random.uniform(0.1, 0.5))

    # scroll down the page
    scroll_amount = random.randint(300, 800)
    await page.mouse.wheel(0, scroll_amount)
    await asyncio.sleep(random.uniform(0.5, 1.5))

    # scroll up slightly
    await page.mouse.wheel(0, -random.randint(50, 150))
    await asyncio.sleep(random.uniform(0.3, 0.8))

Known Limitations and Honest Assessment

Kasada is one of the hardest anti-bot systems to bypass consistently. here are the realities:

  • no pure HTTP solution exists – unlike Sucuri or basic Cloudflare, you cannot bypass Kasada without a real browser. the proof-of-work challenge requires actual JavaScript execution with proper browser APIs.
  • headless mode rarely works – Kasada’s detection of headless environments is very thorough. expect to run headed browsers.
  • scraping speed is limited – because each request requires PoW computation and you need realistic timing, expect 5-15 pages per minute maximum per browser instance.
  • costs are higher – you need premium residential or mobile proxies plus the compute resources to run headed browsers. budget accordingly.
  • maintenance is ongoing – Kasada updates its detection frequently. techniques that work today may need adjustment within weeks.

Summary

bypassing Kasada requires the heaviest tooling of any anti-bot solution:

  • use headed (not headless) Playwright or Puppeteer with extensive stealth patches
  • use premium residential or mobile proxies exclusively
  • add realistic mouse movements, scrolling, and timing
  • intercept API responses from within the browser rather than trying to replay them
  • maintain persistent browser sessions to avoid repeated PoW challenges
  • budget for higher costs in both proxy and compute resources

Kasada is the anti-bot solution where cutting corners costs you the most. trying to use cheaper proxies or simplified approaches will result in near-zero success rates. invest in the proper infrastructure and you can achieve reliable access, but understand that the cost per scraped page will be significantly higher than for sites protected by other WAFs.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top