Python mobile proxy setup
Every mainstream Python HTTP client already supports proxies, so there is nothing to install and no vendor SDK to learn — the integration is one environment variable and one argument. What is worth getting right is the part underneath: reusing connections, retrying properly, and asserting that your exit is where you think it is before a long job commits to bad data.
The problem: the easy part is easy, the durable part is not
Pointing requests at a proxy takes one line, and that line is where most guides stop. The failures show up later: a new TCP connection and a new CONNECT tunnel for every call because the code used requests.get instead of a Session, so a job runs several times slower than it should; a retry loop that hammers a 429 instead of honouring Retry-After; an async client that opens hundreds of concurrent tunnels and is treated as an attack. None of these look like proxy problems in the traceback, which is exactly why they cost so much time.
You will recognise this if
- Throughput far below what the connection should support, with most time in connection setup.
- ProxyError or TunnelError under concurrency that vanishes when you run single-threaded.
- SSL errors after adding the proxy, usually from trying to verify against the proxy rather than the target.
- Escalating 429s because the retry policy backs off too little, or not at all.
- A completed job full of wrong-region data because nothing asserted the exit country.
Working code
Every block below runs as written once PROXY_URL holds a real connection string. Copy yours from the dashboard after your first top-up.
Set the connection URL
bashexport PROXY_URL="http://USERNAME-mbl-us-rot-auto10:[email protected]:7000"
# Same string works for http:// and https:// targets — the client
# issues a CONNECT tunnel for TLS automatically.requests — a Session you can run all day
pythonimport os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
PROXY = os.environ["PROXY_URL"]
session = requests.Session()
session.proxies = {"http": PROXY, "https": PROXY}
retry = Retry(
total=4,
backoff_factor=1.5, # 1.5s, 3s, 6s, 12s
status_forcelist=(429, 500, 502, 503, 504),
respect_retry_after_header=True,
allowed_methods=frozenset(["GET", "HEAD"]),
)
# pool_maxsize keeps tunnels alive instead of reconnecting per request.
session.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=20))
session.mount("http://", HTTPAdapter(max_retries=retry, pool_maxsize=20))
# Assert the exit before the job commits to anything.
who = session.get("https://ipinfo.io/json", timeout=30).json()
print(f"exit {who['ip']} — {who['country']} — {who['org']}")
r = session.get("https://example.com", timeout=30)
print(r.status_code, len(r.content))Session plus pool_maxsize is the difference between one tunnel reused and one tunnel per request.
httpx — sync and async from the same config
pythonimport os, asyncio, httpx
PROXY = os.environ["PROXY_URL"]
# Sync
with httpx.Client(proxy=PROXY, timeout=30.0) as client:
print(client.get("https://api.ipify.org?format=json").json())
# Async — bound concurrency, or you will open a tunnel per task.
async def main(urls):
limits = httpx.Limits(max_connections=10, max_keepalive_connections=10)
sem = asyncio.Semaphore(10)
async with httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=30.0) as c:
async def one(u):
async with sem:
r = await c.get(u)
return u, r.status_code, len(r.content)
return await asyncio.gather(*(one(u) for u in urls))
print(asyncio.run(main(["https://example.com"] * 5)))httpx 0.26+ takes proxy=; older versions used proxies=. Bound concurrency explicitly — unbounded async is what gets a client rate-limited.
aiohttp — proxy goes per request
pythonimport os, asyncio, aiohttp
PROXY = os.environ["PROXY_URL"]
async def main():
conn = aiohttp.TCPConnector(limit=10)
async with aiohttp.ClientSession(connector=conn) as s:
# aiohttp takes proxy= on the call, not on the session.
async with s.get("https://api.ipify.org?format=json", proxy=PROXY) as r:
print(r.status, await r.json())
asyncio.run(main())A common gotcha: setting proxy on ClientSession does nothing — it belongs on each request.
Rotate country or session without touching your code
pythonimport os, re
def with_country(url: str, cc: str) -> str:
return re.sub(r"-(us|gb|de|fr|es|nl|pl|ch|pa|am|ge)-", f"-{cc}-", url, count=1)
def with_session(url: str, sid: str) -> str:
if "-sid-" in url:
return re.sub(r"-sid-[a-z0-9_]+", f"-sid-{sid}", url)
return url.replace("-rot-", f"-sid-{sid}-rot-")
base = os.environ["PROXY_URL"]
print(with_country(base, "de"))
print(with_session(with_country(base, "fr"), "worker07"))Routing lives in the username string, so switching market is a substitution — not a redeploy.
Which rotation mode to use
-rot-auto10For the common Python workload — a crawl of many independent URLs with no session to preserve — auto10 is the right default. You get a fresh carrier IP roughly every ten minutes without any orchestration on your side, which spreads a long job across many exits. Keep using one requests.Session across the rotation: the session is your connection pool and cookie jar, and it is unaffected by the exit changing between requests.
When to choose something else
| Mode | Use it when |
|---|---|
| sticky | Anything stateful — a logged-in account, a multi-step form, a cart. Add -sid- with one identifier per worker. |
| auto30 | Long background jobs where a slower churn is enough and you would rather keep tunnels alive longer. |
| none | Quick one-off scripts where you do not care what the exit does between calls. |
Expected bandwidth use
Plain HTTP from Python is the cheapest way to use the pool, because you download exactly what you ask for and nothing else. This is the workload where a gigabyte goes a very long way.
| Workload | Per unit | Per GB |
|---|---|---|
| HTML page fetch (typical content site) | 100–400 KB | ~2,600–10,000 pages |
| JSON API response | 2–50 KB | ~20,000–500,000 calls |
| HTML with gzip/brotli negotiated | 30–120 KB | ~8,700–35,000 pages |
| HEAD request / status check | ~1 KB | effectively free |
Send Accept-Encoding: gzip, br — requests and httpx do by default, but hand-rolled headers often clobber it, and compression alone typically cuts HTML transfer by 70%. Use HEAD when you only need a status or a header, and stream large responses with stream=True so you can abort once you have what you need instead of paying for the tail. Ranges above are typical; meter your own targets before sizing.
| Bandwidth | Price | Per GB | Save |
|---|---|---|---|
| 1 GB | $7 | $7 | — |
| 5 GB | $35 | $7 | — |
| 10 GB | $68 | $6.80 | 3% |
| 25 GB | $150 | $6 | 14% |
| 50 GB | $283 | $5.66 | 19% |
| 100 GB | $500 | $5 | 29% |
Relevant countries
Change country by editing one token in the proxy username — no redeploy, no second account. Pool depth varies by country and by whether you use the mobile (mbl) or residential (peer) pool; live availability is shown on each country page and in your dashboard.
Legitimate use only
ProxyMobile is sold for measurement, quality assurance and research on systems you own or are authorised to test. Buying bandwidth does not grant permission to access a third party's systems. You remain responsible for complying with each target's terms of service, robots directives and rate limits, and with the data-protection law that applies to you.
For this use case
- Collect from sites you own or are authorised to collect from, within their terms.
- Honour robots directives, Retry-After and any published rate guidance — your retry policy is part of your compliance posture, not just your reliability.
- Bound your concurrency. Unbounded async against one host is indistinguishable from an attack, whatever your intent.
- Do not harvest personal data without a lawful basis, and do not retain what you did not need to collect.
Never, on any plan
- No credential stuffing, account takeover, or any attempt to authenticate as someone else.
- No circumventing an access control, paywall or security measure you have not been authorised to test.
- No collection of personal data you have no lawful basis to process.
- No load that a reasonable operator would experience as a denial of service — throttle, back off on 429s, and honour Retry-After.
Accounts used for fraud, intrusion or bulk abuse are terminated without refund, and we cooperate with valid legal process. Full terms are in our acceptable use policy.
Python FAQ
Do I need a special library or SDK?
No. requests, httpx, aiohttp, urllib3 and Scrapy all support standard HTTP proxies out of the box. You set one proxy URL and everything else in your code is unchanged.
Why is my scraper slow after adding a proxy?
Almost always because each call creates a new connection. Use one requests.Session (or one httpx.Client) for the whole job and raise pool_maxsize, so the CONNECT tunnel is established once and reused instead of per request.
How do I use HTTPS through the proxy?
The same URL works for both schemes — the client issues a CONNECT tunnel automatically for TLS targets. Keep certificate verification on; it validates the target's certificate, not the proxy, so there is no reason to disable it.
What is the difference between proxy= and proxies= in httpx?
httpx 0.26 and later take a single proxy= argument; earlier versions used a proxies= mapping. If you get an unexpected keyword argument error, check your installed version against the one your example was written for.
Why does setting proxy on an aiohttp ClientSession do nothing?
Because aiohttp takes the proxy per request rather than per session. Pass proxy=PROXY to each .get() or .post() call, as in the example above.
Which rotation mode for a Python crawl?
auto10 for stateless crawls of many independent URLs. Switch to sticky with a per-worker -sid- as soon as you carry a login, a cart or any other state between requests.
Related guides
Try it against your own targets
Bandwidth estimates on this page are honest ranges, not promises — the only number that matters is what your targets cost you. Your first top-up is credited with an extra $2, which is enough to meter a real run before you size a budget.
