Web Scraping & Automation

Using Proxies With Python Requests (and httpx, aiohttp)

How to use proxies in Python: requests proxies dict, sessions, environment variables, SOCKS5, rotating proxies, retries, plus httpx and aiohttp examples and common errors.

By Team of Monkeys Editorial Updated 5 min read Affiliate disclosure

This guide contains affiliate links to Geonode, Webshare and Rayobyte. If you buy through them we may earn a commission, at no extra cost to you.

Python requests proxies illustration of a Python script sending requests, httpx and aiohttp traffic through a proxy 3jdej38j4

The basics

Python's requests library takes proxies as a dictionary mapping URL schemes to proxy URLs:

python
import requests

proxy = "http://USER:PASS@gate.example.com:7777"
proxies = {"http": proxy, "https": proxy}

r = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=30)
print(r.json())

Two points that trip people up:

  • The "https" key is the scheme of the target URL, not the proxy. You almost always use an http:// proxy URL for both keys, because HTTPS targets are tunnelled through the HTTP proxy with CONNECT. See how proxies work.
  • Always set a timeout. Without one, a stuck proxy can hang your script forever.

Sessions

Use a Session for connection reuse and default settings:

python
s = requests.Session()
s.proxies.update(proxies)
s.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36"})
r = s.get("https://httpbin.org/ip", timeout=30)

Note: with rotating gateways, connection reuse can keep you on the same exit IP for multiple requests. If you need a new IP per request, use a new session ID in the username or a new Session.

Setting the proxy in your browser (animated GIF) 3jdej38j4
Setting the proxy in your browser. GIF via Tenor

Environment variables

requests respects HTTP_PROXY, HTTPS_PROXY and NO_PROXY:

bash
export HTTPS_PROXY="http://USER:PASS@gate.example.com:7777"
export HTTP_PROXY="$HTTPS_PROXY"
export NO_PROXY="localhost,127.0.0.1"

Explicit proxies= arguments override environment variables. Set session.trust_env = False to ignore them.

Special characters in credentials

URL-encode usernames and passwords that contain special characters:

python
from urllib.parse import quote
proxy = f"http://{quote('user', safe='')}:{quote('p@ss:word', safe='')}@gate.example.com:7777"

SOCKS5

Install the extra: pip install "requests[socks]". Then:

python
proxy = "socks5h://USER:PASS@socks.example.com:1080"   # socks5h = DNS through the proxy
proxies = {"http": proxy, "https": proxy}

Use socks5h rather than socks5 to avoid DNS leaks. See SOCKS5 proxies.

Rotating proxies

With a rotating residential gateway, generate a new session ID for each request or batch:

python
import uuid

def rotating_proxies():
    u = f"http://customer-USER-country-us-session-{uuid.uuid4().hex[:8]}:PASS@gate.example.com:7777"
    return {"http": u, "https": u}
A Python tutorial on rotating proxies for web requests, showing a simple rotation loop you can adapt. Video: NeuralNine on YouTube

With a static list:

python
import random
PROXIES = [l.strip() for l in open("proxies.txt") if l.strip()]

def to_url(line):
    host, port, user, pw = line.split(":")
    return f"http://{user}:{pw}@{host}:{port}"

def random_proxies():
    u = to_url(random.choice(PROXIES))
    return {"http": u, "https": u}

See proxy rotation strategies.

Retries with backoff

python
import time

def get_with_retry(url, attempts=4):
    for n in range(attempts):
        try:
            r = requests.get(url, proxies=rotating_proxies(), timeout=30)
            if r.status_code == 200:
                return r
            if r.status_code == 429:
                time.sleep(2 ** n)
        except (requests.ProxyError, requests.ConnectTimeout, requests.ReadTimeout):
            pass
    raise RuntimeError("all attempts failed")

Proxy tip: If you need a pool to run this against, Geonode works with every tool in this guide: rotate per request for stateless pages, or hold a sticky session for logins and pagination. See using Geonode with Python for working code.

httpx

python
import httpx

with httpx.Client(proxy="http://USER:PASS@gate.example.com:7777", timeout=30) as client:
    print(client.get("https://httpbin.org/ip").json())

Recent httpx versions use proxy=; older versions used proxies=. httpx also supports HTTP/2 (http2=True) and async clients.

aiohttp (async)

python
import aiohttp, asyncio

async def main():
    async with aiohttp.ClientSession() as s:
        async with s.get("https://httpbin.org/ip", proxy="http://USER:PASS@gate.example.com:7777") as r:
            print(await r.json())

asyncio.run(main())

For SOCKS with aiohttp, use the aiohttp-socks package.

TLS fingerprints

Python HTTP libraries have TLS fingerprints that differ from browsers, which some bot protection checks. Libraries such as curl_cffi can impersonate browser TLS fingerprints and accept proxies:

python
from curl_cffi import requests as cffi_requests
r = cffi_requests.get("https://example.com", impersonate="chrome",
                      proxies={"https": "http://USER:PASS@gate.example.com:7777"})

See browser fingerprinting and proxies.

Common errors

ErrorCause
ProxyError: 407Wrong credentials or IP not whitelisted
ProxyError: Cannot connect to proxyWrong host/port, firewall
SSLErrorTLS-intercepting proxy (e.g. unblocker) needs verify=False or its CA certificate
InvalidSchema: Missing dependencies for SOCKS supportInstall requests[socks]
ReadTimeoutSlow exit node; rotate and retry

See common proxy errors.

A reusable Python proxy client

python
import random, time, uuid
import requests

class ProxyClient:
    def __init__(self, gate, user, password, country="us", timeout=30):
        self.gate, self.user, self.password = gate, user, password
        self.country, self.timeout = country, timeout
        self.headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0 Safari/537.36",
            "Accept-Language": "en-US,en;q=0.9",
        }

    def _proxies(self, sticky=None):
        sid = sticky or uuid.uuid4().hex[:8]
        url = f"http://{self.user}-country-{self.country}-session-{sid}:{self.password}@{self.gate}"
        return {"http": url, "https": url}

    def get(self, url, attempts=4, sticky=None):
        for n in range(attempts):
            try:
                r = requests.get(url, proxies=self._proxies(sticky), headers=self.headers, timeout=self.timeout)
                if r.status_code == 200 and "captcha" not in r.text.lower():
                    return r
                if r.status_code == 429:
                    time.sleep(int(r.headers.get("Retry-After", 2 ** n)))
            except requests.RequestException:
                pass
            time.sleep(1.5 ** n + random.random())
        return None

client = ProxyClient("gate.example.com:7777", "customer-USER", "PASS")
resp = client.get("https://httpbin.org/ip")
print(resp.json() if resp else "failed")
How to use and rotate proxies with the Python Requests library, step by step. Video: ScrapeOps on YouTube

Pass a fixed sticky value to keep the same IP across related requests (pagination, logged-in flows).

Async scraping at scale

For many concurrent requests, httpx.AsyncClient or aiohttp with a semaphore keeps concurrency under control:

python
import asyncio, httpx

async def fetch(client, url, sem):
    async with sem:
        r = await client.get(url, timeout=30)
        return url, r.status_code

async def main(urls):
    sem = asyncio.Semaphore(20)
    async with httpx.AsyncClient(proxy="http://USER:PASS@gate.example.com:7777") as client:
        return await asyncio.gather(*(fetch(client, u, sem) for u in urls))

With a rotating gateway, a single async client may reuse connections; create clients per batch or use session IDs if you need guaranteed rotation.

Choosing proxies for Python scraping

Datacenter for lenient sites; residential for protected ones; ISP for logged-in flows. See best proxies for web scraping and cheap proxies.

Budget tip: Webshare is easy to plug into the tools in this guide: point your client at its backbone gateway and add a rotate or session suffix to the username, or download a direct proxy list and rotate it yourself. See using Webshare with Python.

Choosing the right Python HTTP library for proxy work

LibraryStrengths with proxiesWatch out for
requestsSimple, universal, huge ecosystemSynchronous; TLS fingerprint is clearly non-browser
httpxSync and async, HTTP/2, modern APIproxy= vs proxies= differences between versions
aiohttpHigh-concurrency asyncSeparate package for SOCKS
curl_cffiBrowser TLS impersonationDifferent API details from requests
ScrapyFull crawling frameworkIts own middleware model. See Scrapy proxies

For a quick script, requests is fine. For thousands of concurrent requests, httpx or aiohttp. For targets that fingerprint TLS, curl_cffi or a real browser via Playwright.

Debugging checklist

When a Python proxy request fails:

  1. Print the exact proxy URL with the password masked, and check the scheme (http:// vs socks5h://).
  2. Reproduce with curl using the same proxy. See curl proxy.
  3. Check environment variables (env | grep -i proxy) that might override or conflict.
  4. Set timeout=(10, 30) to separate connect and read timeouts, which tells you where it's hanging.
  5. Catch and log the specific exception class (ProxyError, ConnectTimeout, ReadTimeout, SSLError).
  6. Test against a simple endpoint (https://api.ipify.org) before your real target.

Residential tip: For lenient targets, Rayobyte rotating datacenter traffic costs about $0.30 per GB pay as you go, and you can move only the blocked domains to residential. Our guide to using Rayobyte with Python shows the setup.

Keeping secrets out of code

python
import os
proxy = f"http://{os.environ['PROXY_USER']}:{os.environ['PROXY_PASS']}@{os.environ['PROXY_HOST']}"

Load variables from a .env file with python-dotenv in development, and from your platform's secret store in production. Never commit proxy credentials to version control.

For proxy type selection, see best proxies for web scraping and residential proxies.

Proxy settings requests picks up without asking

By default, requests doesn't only read the proxies= you pass. With trust_env on, it also looks at the environment, and through Python's standard library that can include the operating system's proxy settings:

  • Environment variables: HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and NO_PROXY, in upper or lower case.
  • System settings on Windows and macOS: when no proxy variables are set, Python's urllib.request.getproxies() reads the Windows registry or the macOS system configuration, and requests uses the result.
  • .netrc credentials for hosts listed there.

That explains some puzzling behaviour: a script that works on a server but goes through the office proxy on a laptop, or that ignores NO_PROXY set somewhere you forgot about. To see what Python thinks the proxies are:

python
import urllib.request, requests
print(urllib.request.getproxies())
print(requests.utils.get_environ_proxies("https://example.com"))

To make a session use only what you give it:

python
s = requests.Session()
s.trust_env = False
s.proxies = {"http": proxy, "https": proxy}

Explicit proxies= arguments still win over environment values, but trust_env = False removes the surprises entirely. The requests documentation covers proxies in detail. On machines where system proxies are set, see proxy setup on Windows and proxy setup on macOS.

When pages need JavaScript rendering, move from requests to a headless browser; our Puppeteer proxies guide covers that setup. To choose a provider, see the best proxies in 2026.

Choosing between providers for this? Our Webshare vs Geonode for Python comparison shows where each one is stronger, with costs for typical workloads.

For a Webshare-specific walkthrough, see Using Webshare Proxies with Python, which explains the right product, settings and costs for this job.

Rayobyte, our third proxy partner, covers this too: see Using Rayobyte Proxies With Python, or compare all three of our partners in Rayobyte vs Geonode vs Webshare for Python Scrapers.

FAQ

Why does requests ignore my proxy?

Usually a typo in the dict keys (they must be "http" and "https"), or NO_PROXY matching the host.

Should the proxy URL start with http or https?

Usually http://, even for HTTPS targets. Use https:// only if your provider offers TLS connections to the proxy itself.

What proxies are best for Python scraping?

Depends on the target. See best proxies for web scraping.

Sources and further reading

Independent references that back up the points above, from standards bodies, governments, universities and non-profits. They open in a new tab.

Affiliate disclosure: Team of Monkeys is an affiliate of Geonode, Webshare and Rayobyte. Links and banners marked as sponsored, and links to Geonode, Webshare and Rayobyte in the text, use our referral links, and we may earn a commission if you buy through them, at no extra cost to you. Our explanations and comparisons are written to be useful whichever provider you choose.

Editorial note: Proxy providers change their pools, features and prices often, so treat any figures here as typical ranges at the time of writing and confirm current terms with the provider before you buy.