Python · requests, httpx, aiohttp

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.

Start with $2 free creditSee pricingNo subscription · pay per GB

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

bash
export 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

python
import 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

python
import 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

python
import 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

python
import 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

Recommended-rot-auto10

For 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

ModeUse it when
stickyAnything stateful — a logged-in account, a multi-step form, a cart. Add -sid- with one identifier per worker.
auto30Long background jobs where a slower churn is enough and you would rather keep tunnels alive longer.
noneQuick 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.

WorkloadPer unitPer GB
HTML page fetch (typical content site)100–400 KB~2,600–10,000 pages
JSON API response2–50 KB~20,000–500,000 calls
HTML with gzip/brotli negotiated30–120 KB~8,700–35,000 pages
HEAD request / status check~1 KBeffectively 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.

BandwidthPricePer GBSave
1 GB$7$7—
5 GB$35$7—
10 GB$68$6.803%
25 GB$150$614%
50 GB$283$5.6619%
100 GB$500$529%

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.