Last month a teammate came to me with a bug report that sounded impossible. Their scraper logs into a marketplace, walks a paginated order list, and exports rows to CSV. Through a residential pool it ran fine for weeks. Then someone switched the job to the mobile proxy line to see if it would be cheaper per gigabyte, and suddenly about one job in four died halfway through with a session-expired redirect, as if the account had been logged out between pages. Same code, same endpoints, same cookies. The only thing that changed was the exit network.
The job was not being logged out. The IP address was leaving.
This is the single most important thing to understand about mobile proxies before you write a line of code against them: mobile proxy traffic exits through carrier-grade NAT (CGNAT), and the address you borrow is not a stable apartment you rent, it is a seat in a moving bus. Radios hand off between towers, carriers reassign NAT bindings, idle timeouts recycle sessions. Your exit IP can change while your HTTP session, your cookies, and your logic all remain perfectly intact. If your target site pins anything to the IP - and most login flows pin the session cookie to the IP that established it - a mid-job IP change looks exactly like a hijacked session, and the site kills it.
Residential pools churn too, but far more slowly, because a residential exit is usually a home or small-office gateway that stays put for hours or days. Datacenter exits barely churn at all within a session; that is one of their genuine advantages. Mobile churns by design, because the underlying network is a phone network. That instability is not a defect - it is the same property that makes mobile exits look like millions of real phone users to anti-bot systems, since carrier NAT is exactly where real users live. You are trading stability for credibility, and the trade is a good one for some jobs and a terrible one for others.
So before you commit a workload to a mobile pool, measure the churn. Not with vibes, not with "it seemed flaky", but with a ten-minute probe that produces a number you can put in a design doc. Here is the probe I use.
First, the curl version, so you can sanity-check your credentials and sticky-session syntax before writing any code. Most providers expose sticky sessions through the username, something like user-country-us-session-abc123, with a session lifetime the provider documents (Thordata documents theirs as sesstime; check your provider's docs for the exact parameter). Replace the gateway host, port, and credentials with whatever your provider gives you:
# Same sticky session, three requests, ten seconds apart.
# If the "ip" field changes across these three, your sticky
# window is not surviving even ten seconds - check syntax first.
for i in 1 2 3; do
curl -s --proxy "http://USER-session-probe1:PASS@gw.thordata.com:4000" \
"https://api.ipify.org?format=json"
echo
sleep 10
done
If those three responses show the same IP, your sticky syntax works and you can trust the probe below. If they differ, fix the session parameter before drawing any conclusions about the pool.
Now the real probe. The goal is to answer three questions with data:
- How long does a sticky session actually hold one exit IP (time to first rotation)?
- When it rotates, does it stay in the same carrier and metro (harmless churn) or jump across the country (job-breaking churn)?
- What fraction of rotations would break a session that pins cookies to IP?
Here is a runnable Python script. It walks one sticky session forward in time, records every exit IP it sees, and classifies each rotation. It uses only requests and two free echo endpoints (api.ipify.org for the address, ip-api.com for ASN and geo). At probe volumes both are fine without keys.
import time
import requests
from datetime import datetime, timezone
GATEWAY = "http://gw.thordata.com:4000" # your provider's gateway
USER = "USER" # your username
PASS = "PASS" # your password
STICKY = "churnprobe7" # sticky session id
GEO_COUNTRY = "us" # pin country if your plan supports it
PROXIES = {
"http": f"{GATEWAY}".replace("http://", f"http://{USER}-session-{STICKY}-country-{GEO_COUNTRY}:{PASS}@", 1),
"https": f"{GATEWAY}".replace("http://", f"http://{USER}-session-{STICKY}-country-{GEO_COUNTRY}:{PASS}@", 1),
}
ECHO_IP = "https://api.ipify.org?format=json"
ECHO_GEO = "http://ip-api.com/json/?fields=as,isp,city,regionName,country"
def look():
ip = requests.get(ECHO_IP, proxies=PROXIES, timeout=20).json()["ip"]
g = requests.get(ECHO_GEO, proxies=PROXIES, timeout=20).json()
return ip, g.get("as", ""), g.get("city", ""), g.get("regionName", "")
def probe(minutes=30, interval=20):
seen = None
rotations = []
started = datetime.now(timezone.utc)
deadline = time.time() + minutes * 60
samples = 0
while time.time() < deadline:
try:
current = look()
except Exception as e:
print(f"[warn] probe request failed: {e}")
time.sleep(interval)
continue
samples += 1
if seen is None:
seen = current
print(f"[start] ip={current[0]} asn={current[1]} loc={current[2]}, {current[3]}")
elif current[0] != seen[0]:
same_carrier = current[1].split()[0] == seen[1].split()[0]
rotations.append({
"at": datetime.now(timezone.utc).isoformat(),
"from": seen[0], "to": current[0],
"same_asn": same_carrier,
"from_asn": seen[1], "to_asn": current[1],
"from_loc": f"{seen[2]}, {seen[3]}",
"to_loc": f"{current[2]}, {current[3]}",
})
print(f"[rotate] {seen[0]} -> {current[0]} same_asn={same_carrier} "
f"loc {seen[2]} -> {current[2]}")
seen = current
time.sleep(interval)
minutes_run = (time.time() - (deadline - minutes * 60)) / 60
print("\n==== summary ====")
print(f"samples: {samples} over {minutes_run:.1f} min")
print(f"rotations: {len(rotations)}")
if rotations:
same = sum(r["same_asn"] for r in rotations)
print(f"same-carrier rotations: {same}/{len(rotations)}")
for r in rotations:
print(r)
return rotations
if __name__ == "__main__":
probe(minutes=30, interval=20)
What this buys you, and how to read it:
- Time to first rotation tells you the effective session TTL you can promise your jobs. If the probe shows the first rotation at minute 4 with 20-second intervals, then any job that assumes 30 minutes of IP stability through a mobile sticky session is built on fiction, and you now have the receipt.
- Same-ASN rotations are often survivable: the carrier moved your NAT binding, but from the target's perspective you are still "a Vodafone phone in the same metro". Many anti-bot systems weigh ASN consistency heavily. Cross-ASN rotations are the dangerous ones for login-gated jobs.
- Cross-metro, cross-ASN rotations are the ones that will end your authenticated sessions. If those show up early and often, that workload either needs shorter jobs that can re-authenticate cheaply, or it does not belong on a mobile pool at all.
Two practical strategies fall out of the numbers:
Strategy one: design jobs to be rotation-tolerant. Split the work so each unit of work fits inside the measured safe window with margin. If your safe window is 4 minutes, a 90-second work unit with fresh authentication per unit beats a 20-minute job that dies at minute 4. This costs authentication traffic, which on a per-GB plan is a real line item - count it, do not guess it.
Strategy two: detect rotation in-line and treat it as a failure signal, not a retry signal. Before each authenticated request, optionally re-check the echo IP through the same session and compare against the IP recorded at login. If it changed, abandon the session and re-authenticate deliberately, instead of letting the target site return the session-expired redirect and burning your retry budget on requests that were doomed. Here is the minimal guard:
import requests
SESSION_IP_FILE = "session_ip.txt"
def session_ip_still_matches(login_ip):
try:
cur = requests.get("https://api.ipify.org?format=json",
proxies=PROXIES, timeout=15).json()["ip"]
return cur == login_ip, cur
except Exception:
# probe failed: treat conservatively rather than blind-firing
return False, None
ok, current = session_ip_still_matches(open(SESSION_IP_FILE).read().strip())
if not ok:
reauthenticate() # fresh login through the same or new sticky session
with open(SESSION_IP_FILE, "w") as f:
f.write(current or "")
The extra echo request costs a few hundred bytes; the doomed authenticated request it prevents can cost a CAPTCHA, a temporary flag on the account, or a support ticket.
Now the honest costs and limits of this approach:
- The probe itself bills bandwidth. Two echo calls per sample at 20-second intervals is trivial per run, but if you run churn probes continuously in production on a per-GB plan, you are paying to measure. Budget it explicitly. For scale comparison, when I checked today (2026-09-19), Thordata's own pricing page lists mobile proxies starting from $2.2/GB against residential from $0.65/GB, so mobile bandwidth is roughly three and a half times the price - the churn measurement is how you decide whether the credibility premium buys you anything for your specific target.
- Free echo endpoints have limits and moods. ip-api.com's free tier is HTTP-only and rate-limited; if you probe aggressively from many sessions, you will get throttled and your data will lie to you. For anything serious, self-host a tiny echo endpoint on a $5 VPS: a one-line Flask route returning request.remote_addr is the whole deployment.
- One probe session is one sample. Run five or ten sessions on different sticky IDs at different hours before you write conclusions. Carrier behavior differs by country and by carrier; a US probe says nothing about your Indonesia coverage.
- Sticky does not mean pinned-to-one-radio. Providers do their best to hold the session to one exit for the documented window, but the underlying network does not sign contracts. If your probe shows zero rotations in 30 minutes, do not extrapolate to zero rotations in 6 hours.
Where does mobile actually make sense, given all this? In my experience: targets that serve mobile-first traffic and score exit reputation heavily (social platforms, app-adjacent APIs), jobs where being flagged once costs more than the bandwidth premium, and collections where the data itself is mobile-geo-sensitive (app store listings, carrier-specific content). Where it does not: long-lived authenticated crawls with expensive logins, bulk public-page fetches where a datacenter exit is fine, and anything where your measured cross-ASN churn rate times your job length is embarrassing. The number from the probe makes that decision mechanical instead of tribal - and that, more than any particular provider, is the point.
One disclosure: I work with Thordata (their mobile line is what prompted this post), and the pricing figure above was pulled live from their site today; the probe technique is provider-agnostic and works against any gateway that supports sticky sessions.













