Free test available for France , UK or SG on Telegram Join Telegram
Python

Mobile Proxy Python Scraping

Working proxy code for requests, httpx, aiohttp and Scrapy — the proxy dict people get wrong, rotation patterns that survive contact with real sites, and backoff that honours Retry-After instead of hammering a 429.

PXM2 Proxies August 20, 2026 8 min read
4 Libraries covered
Unlimited Bandwidth & rotations
HTTP · SOCKS5 Protocol support
5+ Countries available
  • Works with the standard library stack — requests, httpx, aiohttp and Scrapy need no special client.
  • One proxy URL — the same http://user:pass@host:port string every client understands.
  • Rotation on demand — trigger a new IP from your own code at job boundaries.
  • Unlimited bandwidth — concurrency and payload size never change the bill.
4G / 5G Mobile Proxies Standard Protocols
Protocol supportHTTP(S), SOCKS5
Session typeRotating or sticky
BandwidthUnlimited
HardwareDedicated 4G/5G modem
No Special Client

Ordinary HTTP(S) and SOCKS5 endpoints — no vendor SDK to integrate.

Rotate From Your Code

Trigger a fresh exit IP at job boundaries rather than on a fixed timer.

requests and httpx: Sessions, Proxy Dicts and the CONNECT Tunnel

If the markup you want is already in the initial response, do not launch a browser. An HTTP client is an order of magnitude cheaper in memory and time. Use one Session so the connection pool and cookie jar are reused across requests rather than renegotiating TLS every call:

Python · requests
import os
import requests

PROXY = os.environ["PXM2_PROXY"]   # http://user:pass@host:port

with requests.Session() as s:
    s.proxies.update({"http": PROXY, "https": PROXY})
    s.headers.update({
        "User-Agent": "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 ...",
        "Accept-Language": "en-GB,en;q=0.9",
    })
    r = s.get("https://example.com/listings", timeout=30)
    r.raise_for_status()
Illustrative endpoint and credentials

The detail people get wrong is the https entry. It still uses an http:// scheme, because that scheme describes how to reach the proxy — not the protocol of the target. The client opens a CONNECT tunnel through the proxy and TLS then runs end to end inside that tunnel, so the proxy never sees your plaintext. Writing https:// there instead is the single most common reason a Python proxy appears not to work for HTTPS.

The second detail is consistency: a mobile User-Agent should be paired with a mobile IP. Claiming to be an Android phone from a datacenter range is a contradiction any fingerprinting layer can see.

httpx works the same way with a different keyword — pass proxy to the Client constructor. It is worth preferring when you want HTTP/2, since a browser-like protocol version is one fewer discrepancy between what your headers claim and how your client behaves.

Library Where the proxy goes Rotation hook Best for
requests Session.proxies dict, or per-call proxies= One Session per proxy Straightforward synchronous crawls
httpx proxy= on the Client One Client per proxy HTTP/2, or sync and async in one API
aiohttp proxy= on the individual request Per request, so rotation is trivial High concurrency
Scrapy request.meta["proxy"] A downloader middleware Large structured crawls

Rotating Proxies in Python: Round-Robin, Random and Rotate-on-Failure

There are two quite different situations here, and conflating them causes most of the confusion. If you were given a single rotating endpoint, the rotation happens upstream and your code only decides when to ask for a fresh IP. If you hold a list of endpoints, you own the rotation policy yourself.

For the second case, assign one proxy per Session rather than per request. A Session carries the connection pool and the cookie jar, so swapping its proxy mid-flight defeats the point of having one:

Python · round-robin, one session per proxy
import itertools
import requests

PROXIES = [
    "http://user:pass@host:10001",
    "http://user:pass@host:10002",
    "http://user:pass@host:10003",
]

def sessions_for(proxies):
    "One Session per exit IP, reused for the life of a job."
    out = []
    for p in proxies:
        s = requests.Session()
        s.proxies.update({"http": p, "https": p})
        out.append(s)
    return out

pool = itertools.cycle(sessions_for(PROXIES))

for job in jobs:
    session = next(pool)          # a whole job stays on one IP
    run(session, job)
Illustrative endpoints and credentials

Note what the loop does not do: it never changes IP inside run(). Rotate between logical jobs, never in the middle of one — an exit IP that changes during an authenticated flow looks like session hijacking, which is a worse signal than the scraping was.

Random selection is a reasonable alternative to round-robin when jobs are independent and you want to avoid a predictable cycle. Rotate-on-failure is the pattern worth adding to either: when a proxy starts returning blocks, drop it from the pool rather than continuing to feed it work.

Handling 429s: Retry-After, Exponential Backoff and Jitter

A 429 is the target telling you the rate is wrong. Rotating IP and firing again treats a rate problem as an identity problem, and it is how a crawl escalates from throttled to blocked. Honour Retry-After and back off exponentially:

Python · requests + urllib3 Retry
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

retry = Retry(
    total=5,
    backoff_factor=1.5,               # 1.5s, 3s, 6s, 12s, 24s
    status_forcelist=[429, 500, 502, 503, 504],
    respect_retry_after_header=True,
    allowed_methods=["GET", "HEAD"],
)
s.mount("https://", HTTPAdapter(max_retries=retry, pool_maxsize=20))
Illustrative configuration

Add jitter on top if you are running concurrent workers, or they will all wake up and retry in the same instant — a thundering herd that looks far more mechanical than the original traffic did. And concurrency belongs per target host, not globally: eight parallel requests spread over eight domains is ordinary, while eight against one is a burst.

Retries must be idempotent. status_forcelist plus a POST that creates something will happily create it several times. Restricting allowed_methods to GET and HEAD, as above, is the safe default.

Scrapy and asyncio: Middleware, aiohttp and Concurrency Limits

Scrapy already ships proxy support. Its built-in HttpProxyMiddleware reads request.meta["proxy"], so setting that key in a spider or a small middleware is enough for a single endpoint. For rotation across a list, scrapy-rotating-proxies adds a pool and its own ban tracking:

Python · Scrapy settings.py
ROTATING_PROXY_LIST = [
    "http://user:pass@host:10001",
    "http://user:pass@host:10002",
]
# or: ROTATING_PROXY_LIST_PATH = "proxies.txt"

DOWNLOADER_MIDDLEWARES = {
    "rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
    "rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}

CONCURRENT_REQUESTS_PER_DOMAIN = 4
DOWNLOAD_DELAY = 0.5
AUTOTHROTTLE_ENABLED = True
Illustrative endpoints and credentials

Its default ban heuristic treats a non-200 status, an empty body, or an exception as a dead proxy, which is blunt — a target that answers a block with a 200 and a challenge page will slip straight past it. Override ROTATING_PROXY_BAN_POLICY with a class that recognises what your specific target does when it blocks you.

The two settings at the bottom matter more than the middleware for staying unblocked. AutoThrottle adapts the delay to observed latency, and a per-domain concurrency cap keeps a wide crawl from turning into a burst against one host.

With aiohttp the proxy goes on the individual request rather than the session, which makes per-request rotation trivial and per-host politeness entirely your responsibility. Wrap your fetches in a semaphore keyed by target host — asyncio makes it very easy to turn a polite crawl into a denial-of-service you did not intend.

Get a Dedicated Python Scraping Proxy

Live PXM2 locations — pick the country your target should see the request coming from, and get a dedicated 4G/5G IP with unlimited bandwidth and rotations:

🇫🇷

France

3 Operators 20-150 Mbps
Starting from
$4.34 for 1 hour
4G 5G
Available Operators:
Bouygues Orange SFR
🇮🇳

India

3 Operators 20-30 Mbps
Starting from
$2.74 for 1 hour
4G
Available Operators:
Airtel Jio Vodafone Idea (Vi)
🇸🇬

Singapore

2 Operators 30-70 Mbps
Starting from
$2.99 for 1 hour
4G
Available Operators:
Singtel Vivifi
View all locations →

Frequently Asked Questions

How do I rotate proxies in Python requests?

Keep a list of endpoints and cycle it with itertools.cycle, assigning one proxy per Session rather than per request — a Session carries the connection pool and cookie jar, so swapping its proxy mid-flight defeats the point of having one. For a single rotating endpoint the rotation happens upstream, and your code only needs to decide when to ask for a new IP.

Why is my Python proxy not working for HTTPS?

Almost always because the https entry in the proxies dict was given an https:// scheme. That scheme describes how to reach the proxy, not the protocol of the target, and for an ordinary HTTP proxy it should still be http://. The client then opens a CONNECT tunnel through it and TLS runs end to end inside that tunnel.

How do I use a proxy with aiohttp?

Pass proxy= to the individual request rather than to the session, since aiohttp takes it per call. Credentials go in the URL or in an aiohttp.BasicAuth passed as proxy_auth. Cap concurrency per target host with a semaphore — asyncio makes it trivially easy to turn a polite crawl into a burst that gets you rate limited.

How do I set a proxy in Scrapy?

The built-in HttpProxyMiddleware reads request.meta["proxy"], so setting that key in a spider or a custom middleware is enough for a single endpoint. For rotation across a list, scrapy-rotating-proxies adds ROTATING_PROXY_LIST plus its own middlewares, and tracks which endpoints look banned so it can stop using them.

How do I handle 429 errors when scraping in Python?

Read the Retry-After header and wait that long. Where the server does not send one, back off exponentially with jitter. urllib3’s Retry does both — set respect_retry_after_header and a backoff_factor, mount it on an HTTPAdapter, and resist the urge to rotate IP and retry immediately: a 429 is a rate problem, and treating it as an identity problem is how a crawl escalates to a hard block.

If your target renders its data with JavaScript, the HTTP-client path above will not reach it — the browser guide covers that case.

Web scraping guides

Core mobile proxy guides

Point Your Python Scraper at a Carrier IP

Dedicated 4G/5G modems on ordinary HTTP(S) and SOCKS5 endpoints — no vendor SDK, no bandwidth metering, and rotation you trigger from your own code.

Get a Python Scraping Proxy