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:
- proof of work can’t be faked – you actually need to run the JavaScript and perform the computation
- tokens are single-use – each request requires a fresh proof-of-work token
- 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
- 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 Type | Works with Kasada? | Notes |
|---|---|---|
| Datacenter | Almost Never | Kasada blocks virtually all datacenter IP ranges |
| Shared Residential | Sometimes (~50%) | depends on the residential pool quality |
| Premium Residential | Usually (~75%) | better pool with cleaner IPs |
| ISP/Static Residential | Good (~80%) | consistent IP helps with session tracking |
| Mobile 4G/5G | Best (~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.