Why Your Mobile Proxy Still Gets Blocked: TLS JA3 Fingerprint (2026)
Why Your Mobile Proxy Still Gets Blocked: TLS JA3 Fingerprint (2026)
You put your scraper on a clean mobile ip, a fresh address no blocklist has ever seen, and it still gets blocked on the first request. The ip is perfect. The headers look fine. And the site still throws a captcha or a flat 403. Here’s the part nobody tells you: the site isn’t reading your ip on that request. It’s reading your tls handshake. And Python requests has a handshake fingerprint that no real phone has ever sent.
I run a Singapore mobile proxy farm. Real sims, real Singtel, M1, and StarHub lines sitting in real hardware, not a datacenter pretending to be a phone. Because I watch real customer traffic, I see the jobs that fail even on a flawless ip, and most of the time the ip was never the problem. The client was. So let me walk you through what’s actually happening at the start of every https connection, and why your proxy can be perfect and your scraper still gets caught.
the verdict up front
Here’s the short version so you’re not waiting for it. The proxy fixes your network identity, giving you a real mobile address that looks like a person on a phone. But it can’t fix your tls fingerprint. A raw requests or httpx call has a ja3 that screams “script” the moment the connection opens. A real browser, or a client that impersonates one, has a ja3 that matches a real phone. Your job is to make the fingerprint agree with the ip. Match them, or stay blocked. That’s the whole game.
| Signal | What a raw requests/httpx call sends | What a real phone (or impersonator) sends |
|---|---|---|
| ip | clean mobile ip (the proxy gives you this) | clean mobile ip |
| tls ja3 | an openssl handshake on every known blocklist | a real Chrome/Safari client hello |
| http2 frames | Python’s settings shape | a browser’s settings shape |
| user agent | often contradicts the handshake | matches the impersonated browser |
| result | flagged on the first request | reads as a phone, gets through |
what a tls handshake actually exposes
When your client opens an https connection, it shows its hand before any page loads. Before any header is sent, it sends a message called the client hello, and that message isn’t encrypted. Anyone in the path, including the server, can read it in full. It’s surprisingly detailed: the exact cipher suites your client supports, in the exact order it prefers them, the tls extensions you support, the elliptic curves and the signature algorithms. All of that goes out in the clear before the secure channel even exists.
Here’s why that matters. The specific list, and the specific order, isn’t random. It’s baked into whatever library built the handshake. Chrome on Android sends one very particular ordering. Safari on an iPhone sends a different one. Firefox sends another. And Python requests, sitting on top of openssl, sends a list and an order that looks nothing like any of them. So before you’ve said a single word, the server already knows what kind of software is calling. The ip says phone. The handshake says server library. Those two things disagree, and the disagreement is the tell.
the cipher order is the giveaway
A browser doesn’t just support a set of cipher suites, it advertises them in a fixed priority order, and it adds little decoy entries called grease values shuffled in at known positions. Openssl, the library under requests, orders its ciphers differently and doesn’t lay out grease the way a browser does. So even two clients that support the exact same ciphers can be told apart purely by the order and the padding. Defenders don’t need to break your encryption. They just read the menu you handed them and notice it was printed by a server library, not a phone.
so what is ja3, exactly
Ja3 is a way to turn that client hello into a short fingerprint. You take the tls version, the cipher suites, the extensions, the elliptic curves, and the curve formats, join them in a fixed order into one long string, then hash that string, usually with md5, into a 32-character fingerprint. That hash is the ja3.
The useful thing about it, from the defender’s side, is that it’s stable. Every Python requests install on the planet, on the same version of openssl, produces basically the same ja3, so a site can collect a few known bad hashes and block them forever. The first time anyone ran requests against that site and got flagged, the site learned that ja3, and now every requests user inherits the punishment. You did nothing wrong on your account, your ip is clean, but your handshake matches a hash that was burned months ago by someone else. The proxy gave you a brand new reputation, and your tls library handed it back the instant the socket opened.
There’s a newer variant called ja4 that works on the same principle but is more detailed and harder to fool by accident, less brittle than the old md5 hash. Either way the takeaway holds: a default Python client produces a hash that sits on the known automation list.
httpx doesn’t save you
People switch from requests to httpx thinking it’s more modern, more browser-like, somehow stealthier. It isn’t. Under the hood httpx still leans on the same underlying tls stack, so its ja3 is just as recognizable. You’ve changed the api you write code against, not the fingerprint that goes out on the wire. The server doesn’t care which Python library you imported. It cares what the client hello looks like, and those look like automation.
http2 is a second fingerprint
Ja3 isn’t the only fingerprint anymore. Modern sites also fingerprint your http2 connection. When you speak http2, your client sends a set of settings frames, a window size, a header table size, pseudo-headers in a particular order, and a priority structure. Browsers send a very specific, consistent shape here. Python clients send a different one. So even if you somehow faked a perfect ja3, a mismatched http2 fingerprint can give you away on the next frame. Defenders combine the two: ja3 and http2 have to both look like a browser, together, or the pair looks wrong. It’s the same logic as a clean Singapore 5g mobile proxy, which only helps if the client on top of it behaves like a phone too.
why a real browser behind the same proxy just works
Drive an actual browser through the mobile proxy and the problem disappears. The client hello a real Chromium or Chrome sends is a genuine browser hello: the cipher order is real, the extensions are real, the http2 settings are real. The ja3 matches what billions of phones send every day, because it’s literally the thing those phones send. The proxy gives you the mobile ip, the browser gives you the mobile handshake, and the two halves of your identity finally agree. The site sees a phone on a mobile network, behaving like a phone. Nothing to flag.
the lighter fix: impersonating http clients
You don’t always want a full browser. Browsers are heavy, and for a high-volume scrape you want something lean. This is where impersonating http clients come in, the real fix for non-browser scraping. The best known one is curl-impersonate, a build of curl patched to send the exact tls handshake and http2 fingerprint of a specific browser version. The client hello on the wire is Chrome’s, or Safari’s, byte for byte. There are Python wrappers too, the most popular being curl_cffi, which lets you write code that feels like requests but emits a real browser fingerprint underneath.
from curl_cffi import requests
proxy = "http://USER:PASS@proxy.singaporemobileproxy.com:8000"
r = requests.get(
"https://target.example/",
impersonate="chrome", # pin to a current Chrome build
proxies={"http": proxy, "https": proxy},
headers={
# match the user agent to the browser you impersonate
"User-Agent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/130.0.0.0 Mobile Safari/537.36",
},
)
print(r.status_code)
You point curl_cffi at your mobile proxy, tell it to impersonate a current Chrome on Android, and now you have a lightweight scraper that’s fast like a script but fingerprints like a phone. The ja3 matches. The http2 frame matches. The ip is a real Singapore mobile address. All three signals line up and tell the same story. That’s the combination that actually gets through, where a raw requests call on the same exact proxy would have eaten a 403.
One detail to get right: pin the impersonation target to a real, current browser version. If you impersonate a Chrome that’s two years out of date, it’s technically a browser handshake, but it’s a browser nobody runs anymore, and that staleness is itself a weak signal. Rotate the profile forward when the library ships new browser targets, and treat the version as something you maintain, not set once and forget. The whole point is to blend into the current crowd of real phones, and that crowd keeps updating.
match your user agent to your fingerprint
Here’s the mistake that’s embarrassing when it happens: your user agent string has to match your tls fingerprint. If your handshake says Chrome on Android but your user agent header says Python requests, or a Chrome version that doesn’t exist, you’ve contradicted yourself in two places at once. A decent anti-bot system cross-checks them, and the mismatch is louder than either signal alone. When you impersonate Chrome, set the user agent to that same Chrome. Keep the whole story consistent, top to bottom.
when plain requests is still fine
Not every request needs this. A plain requests call is completely fine in a lot of cases. If you’re hitting a documented public api that wants you there, it doesn’t care about your ja3, it cares about your api key. If you’re talking to a friendly internal endpoint or a partner that expects automated traffic, raw requests is the right tool and impersonation is wasted effort. Fingerprint matching is for the adversarial targets actively trying to keep bots out. Don’t over-engineer a friendly endpoint. Save the heavy artillery for the sites that are actually shooting back, the same ones covered in these web scraping case studies.
check your own fingerprint before you guess
Don’t guess at any of this, measure it. There are free tls fingerprint echo services online. You send a request to one through your proxy, with whatever client you’re testing, and it replies with the exact ja3 it saw, plus your http2 fingerprint and user agent. Run it once with plain requests and write down the hash, then again with curl_cffi impersonating Chrome and compare. You’ll see the ja3 change completely and line up with a real browser. That before-and-after test tells you more than any amount of guessing about why a site blocks you.
the proxy still has to be the right half
Fingerprint and ip have to be a matched pair, which is where the proxy underneath still decides the outcome. Our Singapore mobile ips come off real Singtel, M1, and StarHub sims, so the network side of your identity is genuinely mobile, the same carrier ranges real phones use. You get sticky sessions, so your ip holds steady across a whole login or checkout flow instead of jumping mid-request, and dns routes through the tunnel, so you’re not leaking your real resolver while the rest of you pretends to be in Singapore. You bring a clean browser fingerprint, we bring a clean mobile ip, and the two finally tell the same story. The proxy is half the identity, the fingerprint is the other half, and you need both.
If you want to test it against your real target, there’s a free trial of Singapore Mobile Proxy and code YT30 gets you started. Point your curl_cffi or your real browser at it, run the fingerprint check, and watch the blocks stop.
Get new guides and videos first — join the Telegram channel.