New videos every week — proxies, VPNs & antidetect browsers, explained.

Subscribe
GuidesOct 2, 202610 min read

How to Rotate Proxies in Python

Rotate proxies in Python with requests and httpx: sticky sessions, retries, session pools, and patterns that avoid wasted bandwidth.

Table of contents

Rotating proxies in Python is less about a single magic library and more about a small set of durable patterns: how you configure requests or httpx, when you keep a sticky session versus rotating every call, how you retry after blocks, and how you pool sessions so you do not open a new TCP handshake for every URL. This guide walks through those patterns with sanitizer-safe, copy-ready examples — no invented provider prices, just mechanics you can drop into scrapers, monitors, and research jobs. If you still need a protocol primer, start with SOCKS5 vs HTTP proxies; if you are choosing providers, compare live cards for options like Webshare or Bright Data rather than memorizing outdated rate cards.

What "rotation" actually means

Rotation is a policy for which exit IP handles the next request. Providers expose it via gateway hostnames, username flags, or sticky session IDs. Your Python code should treat that policy as configuration — not hard-code a new IP string on every line.

Core Ideas Before You Write Code

Three knobs control almost every proxy setup:

  • Endpoint: usually a provider gateway like host:port with username/password auth.
  • Session mode: rotating (new exit often) versus sticky (same exit for a TTL or session id).
  • Client behavior: timeouts, retries, connection pooling, and when you rebuild a session.

Datacenter, residential, and ISP proxies all speak HTTP or SOCKS5 to your client; the difference is the IP reputation behind the gateway. For account-like continuity, prefer sticky residential or ISP sessions. For broad crawling where IP churn is acceptable, rotating residential is common. Never fabricate success-rate percentages — measure against your target.

Flowchart labeled REQUEST to GATEWAY to EXIT showing Python proxy rotation path
Your client talks to a provider gateway; the gateway chooses the exit IP according to rotating or sticky policy.

Minimal requests Pattern

The requests library accepts a proxies dict mapping URL schemes to proxy URLs. Keep credentials out of source control — load them from environment variables.

import os
import requests

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

proxies = {
    "http": PROXY_URL,
    "https": PROXY_URL,
}

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

For SOCKS5, install requests[socks] (PySocks) and use a socks5h:// URL so DNS resolves through the proxy. Mixing socks5:// (local DNS) with geo-sensitive targets is a common leak.

# pip install "requests[socks]"
proxies = {
    "http": "socks5h://user:pass@host:1080",
    "https": "socks5h://user:pass@host:1080",
}

httpx Pattern (Sync and Async)

httpx is often cleaner for modern clients: explicit timeouts, HTTP/2 options, and first-class async. Proxy configuration mirrors the idea but attaches to a Client.

import os
import httpx

PROXY_URL = os.environ["PROXY_URL"]

with httpx.Client(proxy=PROXY_URL, timeout=30.0) as client:
    r = client.get("https://httpbin.org/ip")
    r.raise_for_status()
    print(r.json())
import os
import asyncio
import httpx

PROXY_URL = os.environ["PROXY_URL"]

async def main() -> None:
    async with httpx.AsyncClient(proxy=PROXY_URL, timeout=30.0) as client:
        r = await client.get("https://httpbin.org/ip")
        r.raise_for_status()
        print(r.json())

asyncio.run(main())

Watch httpx API versions

Older httpx releases used proxies=... mapping styles; newer ones prefer proxy= on the client. Pin your httpx version and read its docs for your lockfile — do not copy snippets blindly across major versions.

Rotating vs Sticky Sessions

Providers implement rotation differently, but two patterns dominate:

  1. Gateway rotation: every new connection (or every request, depending on product) may get a new exit. Your username stays constant.
  2. Sticky session id: you embed a session token in the username (for example user-session-abc123) so the same exit is held for a TTL.

In Python, sticky mode is usually just a different username string — not a different HTTP client API.

import os
import uuid
import requests

def sticky_proxy(session_id: str) -> dict:
    user = f"{os.environ['PROXY_USER']}-session-{session_id}"
    password = os.environ["PROXY_PASS"]
    host = os.environ["PROXY_HOST"]
    port = os.environ["PROXY_PORT"]
    url = f"http://{user}:{password}@{host}:{port}"
    return {"http": url, "https": url}

# Keep one sticky exit for a logical browsing session
sid = uuid.uuid4().hex[:12]
proxies = sticky_proxy(sid)
session = requests.Session()
session.proxies.update(proxies)
print(session.get("https://httpbin.org/ip", timeout=30).json())

Rotate when your policy says so — new product page batch, new account warm-up, or after a confirmed block — by minting a new session id. Do not rotate mid-checkout on a sticky account workflow.

Split diagram labeled ROTATING and STICKY for Python proxy session modes
Rotating favors breadth; sticky favors continuity. Encode the choice in session ids or gateway flags, not ad-hoc IP lists.

Session Pools Without Thrashing Connections

Creating a new requests.Session or httpx.Client per URL wastes TLS handshakes. Pool a small number of sessions keyed by sticky id or by worker slot.

from collections import OrderedDict
import threading
import requests

class ProxySessionPool:
    def __init__(self, factory, max_size: int = 8):
        self._factory = factory
        self._max_size = max_size
        self._sessions: OrderedDict[str, requests.Session] = OrderedDict()
        self._lock = threading.Lock()

    def get(self, key: str) -> requests.Session:
        with self._lock:
            if key in self._sessions:
                self._sessions.move_to_end(key)
                return self._sessions[key]
            session = self._factory(key)
            self._sessions[key] = session
            if len(self._sessions) > self._max_size:
                _, old = self._sessions.popitem(last=False)
                old.close()
            return session

# factory(key) should return a Session with proxies already set for that sticky key

For asyncio, prefer one AsyncClient per sticky key (or a bounded dict of clients) and close them on eviction. Unbounded client maps are a silent file-descriptor leak.

Retries, Backoff, and When to Rotate

Not every failure deserves a new IP. Timeouts and 5xx often mean retry the same sticky session; 407 means auth is wrong; many 403/429 responses after HTML antibot pages mean rotate or slow down.

import time
import random
import requests
from typing import Callable

RETRYABLE = {408, 425, 429, 500, 502, 503, 504}

def fetch_with_policy(
    url: str,
    session_factory: Callable[[], requests.Session],
    *,
    max_attempts: int = 5,
) -> requests.Response:
    last_exc: Exception | None = None
    for attempt in range(max_attempts):
        session = session_factory()  # may return sticky or freshly rotated
        try:
            resp = session.get(url, timeout=30)
            if resp.status_code in RETRYABLE:
                time.sleep((2 ** attempt) + random.random())
                continue
            resp.raise_for_status()
            return resp
        except requests.RequestException as exc:
            last_exc = exc
            time.sleep((2 ** attempt) + random.random())
    raise RuntimeError(f"failed after {max_attempts} attempts") from last_exc

Wire session_factory so that hard blocks mint a new sticky id, while soft failures reuse the pool entry. Logging the session id (not the password) makes production debugging possible.

Separate auth failures from block failures

A 407 or sudden auth error is not a signal to burn residential bandwidth on rotation. Fix credentials and gateway host first. Rotation is for target-side blocking and reputation — not for misconfigured env vars.

Round-Robin Across Explicit Proxy URLs

Some teams maintain a short list of gateway URLs (different sub-users or ports). A tiny round-robin works — still prefer provider sticky/rotate flags when available.

import itertools
import os
import requests

proxy_urls = [
    os.environ["PROXY_URL_A"],
    os.environ["PROXY_URL_B"],
    os.environ["PROXY_URL_C"],
]
cycle = itertools.cycle(proxy_urls)

def next_proxies() -> dict:
    url = next(cycle)
    return {"http": url, "https": url}

for _ in range(3):
    r = requests.get("https://httpbin.org/ip", proxies=next_proxies(), timeout=30)
    print(r.json())

Concurrency Patterns That Do Not Melt Your Pool

Threading with requests is workable if each sticky key owns a session and you bound workers. Async with httpx scales more naturally but still needs a semaphore so you do not open thousands of concurrent tunnels.

import asyncio
import os
import httpx

PROXY_URL = os.environ["PROXY_URL"]
SEM = asyncio.Semaphore(20)

async def fetch(client: httpx.AsyncClient, url: str) -> int:
    async with SEM:
        r = await client.get(url)
        return r.status_code

async def main(urls: list[str]) -> None:
    async with httpx.AsyncClient(proxy=PROXY_URL, timeout=30.0) as client:
        codes = await asyncio.gather(*[fetch(client, u) for u in urls])
        print(codes)

# asyncio.run(main([...]))

If each task needs its own sticky exit, create clients per task key carefully and cap how many exist at once — identical advice to the sync pool above.

Provider Cards for Rotation Gateways

Pick providers from live data, not from remembered GB prices. These published Proxyaxis entries are common starting points for residential or mixed gateway access; open the cards for current plans and limits.

Webshare

Often used when teams want straightforward HTTP gateways and predictable setup in scripts.

Webshare logo

Webshare

Proxy
4.3
Editor

Webshare is the developer's self-service favorite. Instant activation, a free tier that is actually free, and the cheapest fast datacenter proxies in the market make it the easiest provider to just start using. The custom plan builder — pick your IP count, bandwidth, and threads — is a genuinely good model that lets you avoid paying for capacity you do not need. The residential pool is newer and less battle-tested than specialists, and support is thin on the lower tiers. For datacenter proxies, API-driven workflows, and anyone who wants to try before paying, Webshare is one of the best-value options around.

Bright Data

Enterprise-leaning network with multiple product lines — confirm which gateway product your username targets before coding session flags.

B

Bright Data

ProxyFeatured
4.7
Editor

Bright Data remains the most complete data-collection platform money can buy. No competitor matches its combination of network scale, targeting granularity, and compliance tooling — and for enterprise teams whose revenue depends on reliable data, that completeness justifies the premium. The trade-offs are real: it is one of the priciest providers per gigabyte, the interface overwhelms newcomers, and KYC verification adds friction before you can route a single request. Smaller projects will get better value from Decodo or IPRoyal. But if you need city-level residential targeting at scale, a managed unblocker for the hardest targets, and audit-ready compliance, Bright Data is the default — and our highest-rated proxy provider overall.

Decodo

Frequently evaluated alongside other residential networks for scraping-style workloads.

Decodo logo

Decodo

ProxyFeatured
4.5
Editor

Decodo offers the best price-to-performance ratio in the industry. It delivers roughly 90% of what the enterprise leaders provide — high success rates, a large clean pool, sticky sessions, an unblocker — at a fraction of their cost. The dashboard is the friendliest of any major provider, the 14-day money-back guarantee removes the risk of trying it, and support actually responds. The main gaps are enterprise-grade compliance tooling and the very deepest targeting, neither of which most teams need. For startups, solo developers, and any team that wants professional results without enterprise pricing, Decodo is our top value pick and an easy recommendation.

SOAX

Another published option teams compare when sticky/rotate controls matter to the workflow.

SOAX logo

SOAX

Proxy
4.4
Editor

SOAX is the targeting specialist. City- and ISP-level selection on every plan — not locked behind premium tiers — is genuinely rare, and the continuously cleaned pool keeps success rates high where it matters. It is not the fastest network, the interface could use a refresh, and SOCKS5 coverage is uneven. Those are real but minor gripes against a provider that nails the fundamentals of precision and reliability. For ad verification, localized market research, and social-media work that depends on appearing in an exact location, SOAX is one of the best mid-market options available.

Pattern Best when Python tip
Per-request rotate Broad crawl, low session continuity Reuse one Client; let gateway rotate
Sticky session id Logins, carts, multi-step flows Encode session in username; pool Session
Round-robin URLs Few static gateways / sub-users itertools.cycle over env URLs
Retry then rotate Flaky targets with antibot Backoff first; new sticky id on hard block
Async semaphore High concurrency monitors Bound in-flight requests per process

Testing and Observability

Before you aim at a hard target, verify the path:

  1. Hit an echo IP endpoint and confirm the exit changes when you expect rotation.
  2. Confirm sticky ids hold for the advertised TTL window.
  3. Log latency, status codes, and session keys (never passwords).
  4. Validate DNS behavior for SOCKS (socks5h) if geolocation matters.
  5. Run a soak test at your planned concurrency to catch pool leaks early.

Unit tests should mock HTTP — do not burn paid bandwidth in CI. Integration tests against a paid gateway belong in a gated job with budget caps.

Pitfalls That Waste Bandwidth

  • Embedding passwords in git-tracked notebooks
  • Opening a new Client per request inside a hot loop
  • Rotating sticky account sessions mid-flow
  • Treating every 403 as "need a new IP" when your headers are wrong
  • Ignoring provider concurrency limits until the gateway throttles you
  • Using datacenter exits on targets that clearly expect residential reputation

Stay inside the rules

Only automate and scrape targets you are allowed to access. Rotation is a reliability technique, not permission. Respect site terms, robots rules where they apply, and local law.

Putting It Together: A Small Worker Sketch

A practical worker combines env-based credentials, sticky keys per job, pooled sessions, and conservative retries. Pseudocode shape:

  1. Read gateway settings from the environment.
  2. For each job, mint or reuse a sticky session key.
  3. Borrow a pooled Session/Client for that key.
  4. Fetch with timeouts; retry soft failures; rotate key only on hard blocks.
  5. Emit metrics: success, status, latency, session key, rotate count.

That shape ports from a laptop script to a containerized worker without rewriting your proxy philosophy. For browser-driven flows instead of raw HTTP, see How to Use Proxies with Selenium and keep sticky sessions aligned with profile lifetimes in antidetect setups such as those discussed in Multilogin vs AdsPower.

Environment and Secret Hygiene

Production proxy code fails most often on credential handling, not on HTTP libraries. Prefer a single PROXY_URL or a small set of PROXY_USER, PROXY_PASS, PROXY_HOST, and PROXY_PORT variables injected by your secret manager. Rotate passwords when a laptop image or CI log may have leaked them. Never print full proxy URLs in exception messages — redact passwords before structured logging.

Container images should not bake gateway passwords into layers. Mount secrets at runtime, and keep scrape workers' network egress limited so a compromised job cannot pivot freely inside your VPC.

Conclusion

Rotating proxies in Python is a configuration and policy problem wrapped in a few client APIs. Master requests and httpx proxy wiring, encode sticky versus rotating at the gateway username or product flag, pool sessions so connections stay warm, and retry with intention before burning a new exit. Use live provider cards for commercial choices, measure results on your targets, and keep credentials out of source control. Do that, and rotation becomes a boring, reliable layer — which is exactly what you want in production.

Frequently asked questions

Pass a proxies dict with http and https keys pointing at your gateway URL, or let the provider rotate on each connection while you reuse one Session. For sticky exits, put a session id in the username according to your provider's docs, then keep that Session for the flow.

Both work. requests is ubiquitous and simple for sync scripts. httpx is often better for async and explicit timeouts. Pin versions and follow that release's proxy parameter names so snippets match your lockfile.

A sticky session asks the provider to hold the same exit IP for a TTL or until you change the session id. In code it is usually a username suffix or query flag, not a special Python class. Use sticky mode for logins and multi-step flows.

Rotate on hard blocks, batch boundaries, or when your policy says continuity no longer matters. Do not rotate on every timeout, and never mid-checkout for account workflows. Back off first; then mint a new sticky id if needed.

Install PySocks via requests[socks] or use an httpx SOCKS transport setup, and prefer socks5h so DNS goes through the proxy. Confirm your provider offers SOCKS on the port you configured.

That is almost always bad credentials, wrong gateway host/port, or a username format that omits required provider flags. Fix auth before rotating IPs — rotation will not repair a 407.

Start low, respect your provider plan limits, and raise with a semaphore while watching error rates. Unbounded asyncio gather against a residential gateway is a common way to trigger throttling and wasted bandwidth.

Yes — itertools.cycle over environment-stored proxy URLs works for small static lists. For most residential products, provider-side rotate/sticky flags are cleaner than maintaining long IP lists yourself.

Found this useful? Share it.