← all guides

Getting past Kasada without burning your proxy pool

Most of the “kasada killed my proxies” threads I read are not about proxies at all. The operator is sending a request that fails a check, getting a 429 or a challenge page, rotating to a fresh IP, and failing the same check again. Ten minutes later a pool of a few hundred IPs has a bad reputation on that target and the conclusion is that the vendor is unbeatable. The IPs were fine. The client was wrong, and the rotation logic turned a fixable client bug into a pool-wide burn.

I run mobile and residential pools out of Singapore, and Kasada-protected targets (retail, ticketing, a few sneaker and airline properties) are where I have lost the most IPs per useful request. The stakes are mostly money and time. A burned mobile IP is not free to replace, a sticky session you paid for is gone, and a pool that is flagged on one target often gets a worse score elsewhere too. This piece is about spending IPs deliberately: finding out what is actually failing before you rotate, and building a pipeline where a failure costs you one request and not one IP.

A note on scope. I am talking about collecting public pages at a rate a human-scale site could tolerate, for things like price monitoring and availability checks. I am not covering account takeover, checkout abuse, or anything that gets around authentication. Check the target’s terms and your local rules before you start. This is not legal advice.

background and prior art

Kasada is a bot mitigation vendor, and its approach differs from the older “score the IP and show a CAPTCHA” model. The vendor describes its product at kasada.io as detection that runs on every request rather than relying on challenges shown to humans. In practice, that means the first thing you notice is not a CAPTCHA. It is a bare 429 with a tiny HTML body that loads a script, and no human-readable explanation.

Before Kasada, the usual toolkit was IP reputation, header order, and cookie handling. Those still matter, but they are only the surface. Underneath sits the TLS handshake, which is where most “python requests” setups die. The JA3 fingerprint project from Salesforce documented the idea years ago: hash the ClientHello fields (cipher suites, extensions, curves, point formats) and you get a stable identifier for the client library, independent of the User-Agent header you set. TLS 1.3 itself is specified in RFC 8446, and the HTTP/2 framing that real browsers use is in RFC 9113. A client that claims to be Chrome in its headers but handshakes like OpenSSL with Python defaults is a mismatch a vendor can see before your first byte of HTTP.

I have written before about how pool quality is judged before you commit money to it, in judging pool uniqueness before you commit. The same discipline applies here: measure what you have before you blame it. The wider index of operator write-ups is at /blog/.

the core mechanism

I will describe what I observe from the client side, not what is inside the vendor’s code. Kasada does not publish its internals, and a lot of what circulates in forums is outdated within weeks because the script is regenerated and obfuscated on a rolling basis. Treat anything below as the shape of the system, and verify it on your own traffic.

There are three layers, and they fail differently.

layer 1: network and handshake

This is IP reputation, ASN class, and the TLS/HTTP2 fingerprint. A datacenter ASN starts with a worse prior than a mobile carrier ASN. A TLS fingerprint that does not match any real browser build is an instant strike regardless of ASN. Failures here look like an immediate 403 or 429 on the very first request in a session, with no script ever executing.

layer 2: the client-side script and tokens

When the protected page is requested without valid state, the response is a challenge that loads a script (commonly served from a path ending in ips.js on the protected domain). That script runs in a heavily obfuscated virtual machine, collects browser signals, does a small proof-of-work, and posts the result back to an endpoint on the same domain (commonly seen as /tl). If it passes, you get a token. On later requests, the page’s own JavaScript attaches headers. The ones I see most are:

  • x-kpsdk-ct: the client token issued after a successful challenge
  • x-kpsdk-cd: challenge data, which includes the proof-of-work result and is regenerated per request
  • x-kpsdk-v: a version marker for the script build

Two consequences matter operationally. First, the token is bound to context. Reusing it from a different IP, or with a different TLS fingerprint, or after it has aged out, produces a fresh 429. Second, the per-request cd value means you cannot just copy a header from a browser session into a script and replay it forever.

layer 3: behavioural and rate signals

Even with a valid token, the session is watched. Request cadence that is too regular, no asset requests, paths hit in an order no human uses, and many sessions sharing a token lineage all feed a score. This layer is why a setup that works at 50 requests an hour can die at 500 without anything else changing.

what this means for proxies

Here is the part that saves IPs. A 429 from Kasada is overloaded. It can mean “your handshake is wrong” (layer 1), “you have no valid token” (layer 2), or “you are going too fast” (layer 3). Only the third is partly about the IP’s history. If you rotate on every 429, you are paying IP cost for client-side problems.

So the pipeline I use has three rules:

  • classify every failure before acting on it
  • never rotate the IP on a layer 1 or layer 2 failure, fix the client or re-mint the token on the same IP
  • treat the IP as a budgeted resource with a per-target allowance of failures, not a disposable

For layer 2 specifically, I do not try to reimplement the script’s VM in a plain HTTP client. That is a treadmill. The approach that has held up for me is to let a real browser engine run the vendor script, harvest the resulting state, and then do the cheap, high-volume work through an HTTP client that matches that browser’s TLS fingerprint and uses the same IP. The browser is the expensive part, so you use it to mint sessions and nothing else.

A rough shape of the HTTP side, using curl_cffi for browser-matching TLS and HTTP/2:

from curl_cffi import requests

def fetch(url, session_state):
    # same proxy, same impersonation profile as the browser that minted the state
    r = requests.get(
        url,
        impersonate="chrome",
        proxies={"https": session_state["proxy"]},
        headers=session_state["headers"],
        cookies=session_state["cookies"],
        timeout=20,
    )
    return r

And the classifier that decides what to do with a failure, which is the part that actually protects the pool:

def classify(r):
    if r.status_code == 200:
        return "ok"
    if r.status_code == 429 and "ips.js" in r.text[:2000]:
        return "needs_token"      # layer 2: re-mint on SAME ip
    if r.status_code in (403, 429) and len(r.content) < 500:
        return "handshake_or_ip"  # layer 1: check client first
    if r.status_code == 429:
        return "rate"             # layer 3: back off, same ip
    return "other"

ACTIONS = {
    "ok": "continue",
    "needs_token": "remint_same_ip",
    "handshake_or_ip": "pause_ip_and_audit_client",
    "rate": "backoff_same_ip",
    "other": "log",
}

The key line is pause_ip_and_audit_client. When a brand new IP fails on its first request, I stop sending and test the client against a known-good page before I touch another IP.

worked examples

These are configurations I have actually run, simplified. The cost figures are arithmetic on assumed plan prices so you can substitute your own. The success rates are rough ranges from my own notes on specific targets, not benchmarks, and they will not transfer cleanly to your target or your week.

example 1: python requests on a mobile pool, the expensive mistake

Setup: plain requests with a Chrome User-Agent, 40 sticky mobile ports on a 4G pool, rotating the port on any non-200.

Result: first request to a Kasada-protected retail category page returned 429 on essentially every fresh port. The rotation logic then cycled through all 40 ports in under an hour. Nothing was wrong with the ports. The handshake identified the client as OpenSSL, which no browser uses.

Cost: if a port costs, say, $3 per day on your plan, 40 ports is $120 per day of capacity that returned nothing. Worse, I then saw elevated challenge rates on those IPs against other targets for a few days, which is the real price of burning a clean mobile IP.

Fix: switch to a browser-matching TLS client, and stop rotating on layer 1 failures. Same 40 ports, first-request success went from near zero to a clear majority. The port count was never the constraint.

example 2: browser mints, http client reads

Setup: 6 ports dedicated to a headless-capable browser (I use Playwright with a patched build for fingerprint consistency), 34 ports for HTTP workers. Each browser session loads the target homepage on one port, waits for the token to appear in the request headers, exports cookies and the x-kpsdk-* headers, and hands that bundle plus the port to an HTTP worker that uses the same port.

Numbers: a browser session takes around 6 to 10 seconds including page load and costs far more CPU and bandwidth than the HTTP path. A minted session was good for a few hundred requests before the first token refresh, at a conservative 1 request every 3 to 5 seconds per port. So 6 browser ports feed a lot of HTTP volume, and the expensive path is under 20 percent of total ports.

The thing that mattered: the HTTP worker must use the same IP that the browser used. Early on I let the dispatcher hand tokens to any free port to balance load, and success dropped sharply because the token was bound to the original IP. Pinning token to port fixed it.

example 3: a per-IP failure budget

Setup: same pipeline as example 2, plus a budget. Each IP gets 3 unexplained failures per target per hour. Explained failures (a token expiry I expected, a layer 3 backoff) do not count. Once an IP exhausts its budget, it goes into a cooldown of 6 hours for that target only and keeps working for others.

Result: over a week on a mid-volume retail monitor, the number of IPs I had to retire for good dropped to a handful, compared to the first month where I was replacing around a quarter of the pool. I would not promise those proportions to anyone. What I can say is that the budget forced me to read the failure logs, and most of the “unexplained” failures turned out to be one bug: the HTTP/2 settings frame differed slightly between my client profile and the browser version that minted the token.

If you run a lot of accounts or identities on top of this and the browser fingerprint side gets complicated, the antidetectreview.org blog covers the browser profile tooling in more detail than I want to here, and multiaccountops.com has operational writing on keeping many sessions separated.

edge cases and failure modes

the token that works once

Symptom: a minted session returns 200 for a handful of requests, then 429 with the challenge script again.

Likely causes: token expiry, or a version bump on the script (the x-kpsdk-v marker changes) that invalidates tokens minted by the old build. Counter: store the version marker with the session, and treat a changed marker as a hard signal to re-mint on the same IP. Do not rotate.

version skew between browser and http client

Symptom: browser passes, HTTP client with the harvested state fails immediately, even on the same IP.

Cause: the impersonation profile in the client does not match the browser build that ran the script. Header order, HTTP/2 settings, ALPN, even the cipher list can differ. Counter: pin versions. Pick one browser version, one client impersonation target, and update them together. I keep a tiny canary that fetches a protected page through the full path every 15 minutes and alerts when it fails, so a drift shows up as one failed canary and not as 2,000 failed jobs.

geolocation and language mismatch

Symptom: sessions from a Singapore mobile IP that send Accept-Language: en-US and a US timezone in the browser signals get worse scores than sessions that look local.

Counter: make the browser’s locale, timezone, and Accept-Language consistent with the IP’s location. This is a consistency issue, not a trick. For client hints, the mechanism is specified in RFC 8942, and the headers your client sends should agree with what the browser would send for the same version.

shared token lineage across ports

Symptom: everything works for a day, then a whole group of ports degrades at once.

Cause: you cloned one browser profile or one token across many ports, so the vendor sees many IPs sharing the same lineage. Counter: one browser session per port, fresh profile each time, no sharing. This costs more browser time, and it is worth it.

retry storms

Symptom: a brief upstream hiccup (the target returns 503s for two minutes) and your retry logic multiplies traffic, which then trips layer 3 and gets IPs flagged.

Counter: exponential backoff with jitter, a global concurrency cap per target, and a circuit breaker that stops all traffic to a target when the failure rate crosses a threshold. A 503 is not a Kasada failure and must never feed the IP failure budget. The semantics of status codes are in RFC 9110 if you want the canonical reading of what 429 and 503 are meant to signal.

what we learned in production

The biggest shift was treating IPs as inventory with an audit trail rather than a stream. Every request logs the IP, the target, the session id, the classifier output, and the client version. When something degrades, I can answer “is this the IPs, the client, or the token” with a query instead of a guess. Nearly every time I thought a pool was burned, the log said the client or token was the problem, and the IPs recovered the moment I fixed it. The pools I did lose were the ones I rotated through blindly in the first hour of an incident.

The second lesson is to be slower than you think you need to be. Layer 3 is forgiving of slow, boring traffic and unforgiving of bursts. Cutting per-port rate by half and adding more ports was cheaper than the replacement cost of burned IPs, and the data was just as fresh for price monitoring purposes. If the data you need changes hourly, you do not need a request every second. For people running this alongside airdrop or multi-account workflows, the sister site airdropfarming.org has notes on pacing that apply the same logic. Nothing here guarantees results, since vendors change things constantly, and you should expect to re-verify the setup every few weeks.

Written by Xavier Fok

references and further reading

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-07.

proxies
Need proxies that survive the block wall?

Singapore Mobile Proxy runs real 4G/5G mobile IPs on rotating SIMs — the carrier-grade addresses most of these targets still trust.

see plans →
read on
More scraping guides

The rest of the field manual: target-site playbooks, library walkthroughs, provider reviews, and anti-bot troubleshooting.

browse all guides →