How to Use and Rotate a Proxy Server in Python (2026)
TL;DR
requests routes traffic through a proxy with a plain dict. Pass proxies={"http": "...", "https": "..."} to any request, or set session.proxies once on a requests.Session() and every call on that session inherits it.
One proxy runs out of road fast. A single IP gets rate-limited or blocked after a handful of requests to most sites; rotating across multiple exit IPs — either a list you maintain or a provider-managed gateway — is what keeps a script running past that point.
SOCKS5 needs one extra package, not a different approach. Installing requests[socks] (PySocks under the hood) lets the same proxies dict work with a socks5h:// URL instead of http://.
urllib3.util.Retry turns transient proxy failures into automatic retries. Mounting a Retry(total=3, backoff_factor=0.3, status_forcelist=[502, 503, 504]) policy on an HTTPAdapter means a dropped connection or a 503 gets retried with exponential backoff instead of raising immediately.
aiohttp uses the same proxy string format as requests. Swapping to aiohttp.ClientSession with asyncio.gather lets a script hit a rotating pool of proxies concurrently, which cuts wall-clock time for any bulk job.
A rotating gateway removes the list-maintenance problem entirely. Providers such as Nstproxy expose one fixed host:port and rotate the exit IP server-side based on a session or time value encoded in the proxy username, so the client code no longer needs to track which IPs are alive.
Introduction: connecting Python to a proxy server
A Python script that talks to a proxy server is just a script whose requests or aiohttp call is pointed at an intermediary address instead of the target site directly — the proxy forwards the request and the response comes back through the same hop. That one architectural change is why proxies show up in almost every Python project that does sustained HTTP work: monitoring a competitor's public pricing page from a fixed IP triggers a block within minutes, but the same job spread across rotating IPs keeps returning 200.
This guide covers the parts of that workflow competitor tutorials tend to skip: verifying that retry logic actually recovers from a dropped proxy connection, running the same rotation pattern concurrently with aiohttp, and the difference between a self-maintained IP list and a provider-managed rotating gateway. Every code block below was executed against a real local proxy before being written down — see the verification notes inline where a step depends on credentials this guide can't ship.
Take a Quick Look
Maintaining and health-checking your own proxy list turns into its own side project once a script needs more than a handful of requests a minute — Nstproxy's Residential Lite gateway takes over that rotation server-side, so your Python code just points at one host:port.
Three packages cover every pattern in this guide: requests for synchronous calls, requests[socks] for SOCKS5 support, and aiohttp for concurrent rotation.
pip install requests "requests[socks]" aiohttp
requests[socks] pulls in PySocks, which is what actually implements the SOCKS4/SOCKS5 handshake — requests itself only knows how to hand a connection off to it. Skipping this extra and using a socks5:// URL directly raises MissingSchema or a dependency error, not a proxy connection failure, which is a common point of confusion when a script "can't find" a working SOCKS proxy.
Configure a proxy for a single request or a whole session
A proxy in requests is a dictionary that maps each URL scheme to a proxy address, and the cleanest way to reuse it is to set that dictionary once on a Session rather than passing it to every call.
import requests
PROXY_URL ="http://username:password@proxy-host:proxy-port"proxies ={"http": PROXY_URL,"https": PROXY_URL}resp = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=15)# per-request onlysession = requests.Session()session.proxies = proxies
resp = session.get("https://httpbin.org/ip", timeout=15)# every call on this session reuses it
Embedding credentials as username:password@host:port is the same authenticated-proxy format used across requests, curl, and most proxy providers' own documentation. If a password contains @, :, or other reserved URL characters, run it through urllib.parse.quote() before building the string — an unescaped special character is a frequent cause of ProxyError that looks like a wrong password but is actually a malformed URL.
This session pattern, plus a Retry-wired adapter and a rotation pool from the next section, was run against a local authenticated proxy (proxy.py, basic auth) with an allowlisted HTTPS target: two sequential calls on the same session both returned 200, and the same call with intentionally wrong credentials failed with the expected 407 Proxy Authentication Required, confirmed via requests.exceptions.ProxyError.
requests also reads the HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables automatically when session.trust_env is True (the default), which is documented behavior in the requests proxies reference — worth knowing because a proxy set in the shell environment silently overrides one set in code unless trust_env is turned off.
Basic implementation: retries and a rotating IP list
A Session on its own doesn't retry anything — that behavior comes from mounting a Retry policy on an HTTPAdapter, and rotation on top of that just means picking a different proxy string before each call.
backoff_factor=0.3 means urllib3 sleeps 0.3 * (2 ** (retries - 1)) seconds between attempts — roughly 0.3s, 0.6s, 1.2s — capped by backoff_max (120 seconds by default), per the urllib3 Retry reference. status_forcelist is what makes a 503 retry automatically instead of returning immediately; without it, Retry only reacts to connection-level failures, not HTTP status codes.
Verification: this exact retry-wired-session pattern and a four-request rotation loop across two local proxy instances both ran live against an allowlisted target, returning 200 on every call and alternating between the two proxy endpoints as expected from random.choice().
Advanced patterns: gateway rotation, sticky sessions, and async
The rotation pattern above assumes a Python script owns and refreshes PROXY_POOL — a provider-managed rotating gateway removes that responsibility by handing the client one fixed address and moving the rotation logic server-side.
Nstproxy's residential gateway is a documented example of this shape: a client connects to a single host:port generated from the Channel page in the dashboard, and the exit IP changes according to parameters encoded directly in the proxy username, not in application code. The block below is illustrative until real credentials are supplied — GATEWAY_HOST, GATEWAY_PORT, CHANNEL_ID, and PASSWORD all come from that Channel page, not from this guide.
The r_10m segment sets a timed rotation window — Nstproxy documents a configurable range of 1 to 120 minutes — and swapping the session identifier (s_session123) to a new value forces an immediate new exit IP, which is the sticky-session pattern for a login or checkout flow that needs the same IP across several steps but a fresh one for the next run. Setting r_10m to per-request rotation instead returns a new IP on every call with no session tag needed at all.
Nstproxy is a proxy infrastructure provider built around this gateway model, aimed at Python and Node.js developers who need rotation without maintaining a list. Its Residential Lite line is the entry point for this kind of work: prepaid packages from 10GB, priced from $1.00/GB with no subscription auto-renewal, backed by a pool the provider states at 50M+ residential IPs across 200+ countries and regions with a stated 99.5% success rate. Selection tradeoff worth knowing up front: Residential Lite is priced for scripts that can tolerate occasional slower hops in exchange for lower cost, not for latency-sensitive real-time use.
Gateway-based rotation — one host:port for the entire pool; the provider rotates exit IPs server-side, so PROXY_POOL list-maintenance code becomes unnecessary.
Country and session targeting in the username — country and session parameters are set by editing the username string, with no separate API call needed per request.
HTTP, HTTPS, and SOCKS5 on the same channel — the documented gateway supports all three protocols, so the requests[socks] pattern from earlier works against the same host by changing only the URL scheme.
For concurrent rotation, aiohttp takes a proxy keyword per request instead of a proxies dict, and asyncio.gather runs a batch of them at once:
asyncio.gather schedules every coroutine passed to it and runs them concurrently rather than one after another, which is the documented behavior in the Python asyncio task reference. Verification: six concurrent requests through this exact pattern, spread across a two-proxy local pool, all returned 200 with the destination alternating between both proxy endpoints.
SOCKS5 uses the same proxies dict shape as HTTP, just with a socks5h:// scheme (the trailing h means DNS resolution happens through the proxy rather than locally, which matters for hiding the target hostname from the client's own network):
This block was run live against a local SOCKS5 server (pproxy, with authentication) and returned 200. SOCKS5 is defined by RFC 1928 as a general-purpose protocol that forwards TCP traffic without inspecting it, which is why the same SOCKS5 endpoint can carry HTTP, HTTPS, or other TCP-based traffic without protocol-specific handling in the client — see Nstproxy's own explainer on SOCKS5 versus HTTP proxies for how that plays out for scraping and automation traffic specifically.
Honest limits of proxying at the requests/aiohttp layer
Everything above changes which IP a request leaves from — it does not change what comes back, and that boundary causes most of the surprises in production scripts.
A proxy has no effect on JavaScript-rendered content: requests and aiohttp return the raw HTML a server sends, so a page that builds its content client-side needs a headless browser (Playwright or Selenium, both of which accept the same proxy string format) regardless of how well the rotation is configured. Proxy latency also adds up under concurrency — residential IPs typically add tens to a few hundred milliseconds per hop compared to a direct connection, so a thousand-URL job at high concurrency is bounded by proxy response time as much as by target-site response time. Rotating IPs does not override a target site's terms of service or robots directives; check what a specific site's terms allow before pointing a rotation script at it, and keep request volume proportional to what a human user of that site would generate. Finally, Retry with status_forcelist retries HTTP-level failures automatically, but it will not distinguish a genuinely dead proxy from a target site that is rate-limiting that specific IP — production code still needs to drop consistently-failing proxies out of rotation rather than retrying them forever.
Troubleshooting common proxy errors in Python
A 407 Proxy Authentication Required, raised as requests.exceptions.ProxyError, means the proxy rejected the username or password in the URL — confirmed above by intentionally sending wrong credentials and observing the same error. A bare ProxyError with Connection refused instead means the host or port is wrong, or the proxy service is down; a ConnectTimeout on the same call usually means a firewall or network path is dropping the connection silently rather than rejecting it outright, which reads differently in logs even though both look like "the request never finished." An SSLError through a proxy is almost always the proxy performing a TLS interception it isn't configured to trust, or a socks5h:// proxy where the target's certificate doesn't match what the resolver returned — switching to socks5:// to test whether local DNS resolution changes the failure mode helps isolate that from the certificate itself. If a proxy repeatedly returns 200 with a page that says "access denied" or a CAPTCHA rather than raising an HTTP error, that is not a connection problem at all — it's the target site detecting automated traffic despite a working proxy, which retry logic alone won't fix.
Conclusion
A Python proxy setup starts with the same proxies dict for every request or session, and everything past that — retries, rotation, SOCKS5, async concurrency — is additive on top of that one pattern rather than a different approach. Where a script lands on the self-maintained-list-versus-managed-gateway question mostly comes down to how much IP-list upkeep is worth avoiding for the request volume involved.
Q: Do I need to set the proxy on every requests call, or can I set it once?
Set it once on a requests.Session() via session.proxies = {...}; every call made on that session object reuses the same proxy and connection pool without repeating the dictionary.
Q: Why does my proxy code raise a 407 error?
A 407 Proxy Authentication Required means the proxy rejected the username or password embedded in the proxy URL — check for unescaped special characters in the password first, since those are the most common cause of a credential that looks correct but isn't parsed correctly.
Q: Can I use the same rotation logic with aiohttp instead of requests?
Yes — aiohttp.ClientSession.get() takes a proxy keyword argument instead of a proxies dict, and running several of those calls through asyncio.gather() rotates across a proxy pool concurrently rather than one request at a time.
Q: Is SOCKS5 better than an HTTP proxy for Python scripts?
Neither is strictly better; SOCKS5 is protocol-agnostic and forwards any TCP traffic without inspecting it, which suits non-HTTP protocols or DNS-through-proxy resolution (socks5h://), while an HTTP proxy is simpler to set up and sufficient for straightforward HTTP/HTTPS requests.
Q: What's the difference between rotating a list of IPs myself and using a rotating gateway?
A self-maintained list requires sourcing, health-checking, and refreshing IPs in your own code, while a rotating gateway (one fixed host:port) moves that rotation logic server-side, at the cost of depending on the provider's gateway uptime instead of your own list.
Q: Does adding retries with urllib3.util.Retry fix a blocked or banned proxy?
No — Retry recovers from transient failures like connection resets or 502/503/504 responses, but a proxy that is IP-banned by the target site will keep returning the same failure on every retry, so production code needs separate logic to drop persistently failing proxies out of rotation.
Marcus Chen
Aug. 6th 2026
110M+ real IPs with 99.9% access success
Blazing-fast average response ~0.5s for high-concurrency tasks
From only $0.1/GB
Get immediate access to premium residential, datacenter, IPv6 and ISP proxy pools.