Quick answer
How do I use rotating residential proxies in Python?
Pass http://USERNAME:[email protected]:7777 as the proxy for both http and https in requests, as proxy= in httpx 0.26+, or as proxy= per request in aiohttp. CrawlRelay residential proxies rotate on every new connection and stay sticky with -session-<id> in the username. They cost $1.50 for 1 GB and $0.50/GB from 50 GB. Paid in crypto, with no KYC.
Python setup at a glance
| requests | proxies={"http": url, "https": url}; retries through urllib3 Retry |
|---|---|
| httpx | httpx.Client(proxy=url); the old proxies= argument was removed in httpx 0.28 |
| aiohttp | session.get(url, proxy="http://USER:PASS@host:port"), credentials in the proxy URL |
| SOCKS5 | pip install "requests[socks]" or httpx[socks], then socks5h:// on port 7778 |
| New IP per request | Disable keep-alive: Connection: close, max_keepalive_connections=0 or force_close=True |
| Same IP for a flow | -session-<id>-ttl-<minutes> in the username, up to 30 minutes |
| Proxy URL scheme | Always http:// for the HTTP port, even for HTTPS websites |
How do I use a proxy with requests, with retries that respect Retry-After?
Mount an HTTPAdapter with urllib3's Retry on a Session. It retries connection errors and the statuses you list, waits with exponential backoff, and honours the Retry-After header that sites send with 429 and 503. Set trust_env = False so a leftover HTTPS_PROXY variable cannot override your proxy.
import os
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
USER = os.environ.get("CRAWLRELAY_USER", "USERNAME-country-us")
PASSWORD = os.environ.get("CRAWLRELAY_PASSWORD", "PASSWORD")
PROXY = "http://%s:%[email protected]:7777" % (USER, PASSWORD)
retry = Retry(
total=4,
connect=2,
backoff_factor=1.0, # waits of 0 s, 2 s, 4 s, 8 s (Retry-After wins on 429/503)
status_forcelist=(429, 502, 503, 504),
allowed_methods=frozenset({"GET", "HEAD"}),
respect_retry_after_header=True,
)
session = requests.Session()
session.trust_env = False # ignore HTTP(S)_PROXY from the environment
session.mount("http://", HTTPAdapter(max_retries=retry))
session.mount("https://", HTTPAdapter(max_retries=retry))
session.proxies = {"http": PROXY, "https": PROXY}
session.headers["User-Agent"] = "ExampleCrawler/1.0 (+https://example.com/crawler-info)"
session.headers["Connection"] = "close" # new connection per request, so usually a new exit IP
r = session.get("https://api.ipify.org", timeout=(10, 30))
print(r.status_code, r.text)How do I build targeted proxy URLs safely?
Generate the username in one function and validate it. A typo in a country code or an unencoded character in the password produces a 407 that looks like a credentials problem. The helper below keeps the documented order (USERNAME-country-{cc}-state-{state}-city-{city}-zip-{zip}-asn-{asn}-session-{id}-ttl-{minutes}), refuses unknown keys and URL-encodes both credentials.
import re
from urllib.parse import quote
ORDER = ("country", "state", "city", "zip", "asn", "session", "ttl")
VALUE = re.compile(r"^[a-z0-9_]+$")
def build_username(base, **params):
"""build_username("USERNAME", country="us", city="new_york", session="job1", ttl=30)"""
parts = [base]
for key in ORDER:
value = params.pop(key, None)
if value is None:
continue
value = str(value).lower()
if not VALUE.match(value):
raise ValueError("bad value for %s: %r" % (key, value))
if key == "ttl" and not 1 <= int(value) <= 30:
raise ValueError("ttl must be 1 to 30 minutes")
parts.append("%s-%s" % (key, value))
if params:
raise ValueError("unknown parameters: %s" % ", ".join(sorted(params)))
return "-".join(parts)
def proxy_url(username, password, socks=False):
scheme, port = ("socks5h", 7778) if socks else ("http", 7777)
return "%s://%s:%[email protected]:%d" % (
scheme, quote(username, safe=""), quote(password, safe=""), port)
print(proxy_url(build_username("USERNAME", country="us", state="ny", city="new_york", session="job1", ttl=20), "p@ss:word"))How do I get a new IP on every request with httpx?
Turn off keep-alive. httpx pools connections by default, and a reused connection keeps its exit IP. With max_keepalive_connections=0, each request opens a new connection to the gateway and usually leaves through a different residential IP. num_bytes_downloaded gives the bytes received on the wire, which is what traffic billing is closest to.
import httpx
PROXY = "http://USERNAME-country-gb:[email protected]:7777"
# No keep-alive: every request opens a new connection, so it usually gets a new exit IP
with httpx.Client(
proxy=PROXY,
timeout=httpx.Timeout(30.0, connect=10.0),
limits=httpx.Limits(max_keepalive_connections=0),
follow_redirects=True,
) as client:
for _ in range(3):
r = client.get("https://api.ipify.org")
print(r.text, r.num_bytes_downloaded, "bytes on the wire")How do I scrape concurrently with asyncio without hammering one site?
Cap concurrency per host, not only globally. A global limit of 50 lets all 50 requests hit the same site when your URL list is sorted by domain. A semaphore per hostname keeps each site at a polite level while the crawl as a whole stays fast. Start at 2 to 4 per host and read the concurrency guide before you raise it.
import asyncio
from collections import defaultdict
from urllib.parse import urlsplit
import httpx
PROXY = "http://USERNAME-country-us:[email protected]:7777"
PER_HOST = 4 # parallel requests per target host
host_slots = defaultdict(lambda: asyncio.Semaphore(PER_HOST))
async def fetch(client, url):
async with host_slots[urlsplit(url).hostname]:
try:
r = await client.get(url)
return url, r.status_code, r.num_bytes_downloaded
except httpx.HTTPError as exc:
return url, type(exc).__name__, 0
async def main(urls):
limits = httpx.Limits(max_connections=50, max_keepalive_connections=0)
timeout = httpx.Timeout(30.0, connect=10.0)
async with httpx.AsyncClient(proxy=PROXY, limits=limits, timeout=timeout) as client:
for result in await asyncio.gather(*(fetch(client, u) for u in urls)):
print(*result)
asyncio.run(main(["https://example.com/", "https://example.org/", "https://example.net/"]))How do I use a proxy with aiohttp?
Pass the gateway as proxy= on each request, with the credentials in the proxy URL. That works on every aiohttp 3.x release without warnings; BasicAuth and the proxy_auth= parameter are deprecated since aiohttp 3.14 and will be removed in 4.0. A TCPConnector(force_close=True) closes every connection after its response, so each request usually gets a new exit. Without it, aiohttp reuses connections and you keep the same IP for a while.
import asyncio
import aiohttp
# Credentials go in the proxy URL; percent-encode a password that contains @ or :
PROXY = "http://USERNAME-country-fr:[email protected]:7777"
async def main():
# force_close=True: one connection per request, so each request usually gets a new exit IP
connector = aiohttp.TCPConnector(limit=20, force_close=True)
timeout = aiohttp.ClientTimeout(total=60, sock_connect=10)
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
for _ in range(3):
async with session.get("https://api.ipify.org", proxy=PROXY) as resp:
print(resp.status, await resp.text())
asyncio.run(main())How do I keep one IP for a multi-step flow?
Generate one session ID per flow and put it in the username with a TTL longer than the flow. Keep-alive is fine here: you want the same exit. If the residential device behind the session goes offline, the IP can change before the TTL ends, so make the flow restartable from its last checkpoint.
import uuid
import requests
session_id = uuid.uuid4().hex[:10] # one ID per flow, lowercase letters and digits
user = "USERNAME-country-us-state-ca-session-%s-ttl-20" % session_id
proxy = "http://%s:[email protected]:7777" % user
with requests.Session() as s:
s.proxies = {"http": proxy, "https": proxy}
first = s.get("https://api.ipify.org", timeout=(10, 30)).text
second = s.get("https://api.ipify.org", timeout=(10, 30)).text
print(first, second, "same exit" if first == second else "exit changed")How much traffic does a Python scraper use per page?
Measure it on 100 real pages before you buy volume. Log num_bytes_downloaded in httpx, or the length of the compressed body in your own middleware, and multiply by your page count. HTML-only requests usually cost tens of kilobytes per page; the bandwidth guide gives the numbers and the cost per 1,000 pages at CrawlRelay prices.
Three habits cut traffic without changing your results:
- Send
Accept-Encoding: gzip, br(httpx and requests send gzip by default) so pages travel compressed. - Fetch the JSON endpoint that a page loads, when the site documents or allows it, instead of the full HTML.
- Skip unchanged pages with
If-None-MatchorIf-Modified-Since: a304 Not Modifiedcosts a few hundred bytes.
Python and proxies: questions
requests, httpx or aiohttp: which should I use for scraping?
Use requests for simple scripts and small crawls. Use httpx when you want the same API in sync and async code, and HTTP/2 through httpx[http2]. Use aiohttp for very high concurrency in pure asyncio. For a crawl with scheduling, deduplication and pipelines, use Scrapy instead of writing those parts yourself.
Why do I get SSL: WRONG_VERSION_NUMBER with my proxy in Python?
You probably wrote https:// in front of the proxy address. The gateway's port 7777 speaks plain HTTP and opens a CONNECT tunnel for HTTPS sites, so the proxy URL must start with http:// in both the http and the https keys. See the error page.
How do I use SOCKS5 proxies in Python?
Install the extra (pip install "requests[socks]" or pip install "httpx[socks]"), then use socks5h://USERNAME:[email protected]:7778. The h sends hostnames to the proxy, so DNS resolves at the exit and you see the same regional answers as local visitors.
Why does my rotating proxy return the same IP twice?
Your client reused a kept-alive connection, and rotation happens per new connection. Send Connection: close in requests, set max_keepalive_connections=0 in httpx, or use force_close=True in aiohttp. Also check that the username has no session- part, which keeps one IP on purpose.
How do I set timeouts for proxied requests?
Set them explicitly: requests has no timeout by default. A connect timeout of about 10 seconds and a read timeout of 30 seconds suit HTML pages; mobile exits and slow targets need 45 to 60 seconds. Retry a timeout once on a new connection rather than waiting longer. See read timeouts.
Can I use these proxies in Jupyter or with pandas?
Yes. Anything that uses requests or httpx underneath takes the same proxy settings. For pandas.read_html or read_csv on a URL, download the content with a proxied requests call first, then pass the text or bytes to pandas.
Run your Python scraper through residential IPs
$1.50 for 1 GB, $0.50/GB from 50 GB.