DEV Community

Greta
Greta

Posted on

Integrating Rotating Proxies into requests: Session-Level, Per-Request, or a Custom Adapter?

Integrating Rotating Proxies into requests: Session-Level, Per-Request, or a Custom Adapter?

The requests library makes "use a proxy" look trivial: pass proxies={"https": "http://..."} and you're done. Then reality arrives. You need a different residential IP on every request. Or the same IP for a five-call login flow and a different one after. Or a retry that swaps to a fresh gateway when the current one times out. At that point the one-liner proxies dict starts to feel like it was designed for a corporate squid box, not a rotating pool of thousands of gateways. This article walks through the three real integration strategies — per-request, session-level, and a custom HTTPAdapter — and when each one is the right tool.

Strategy 1: Per-request proxies= (fine for simple rotation)

The most direct mapping to a rotating pool: build a fresh gateway string on every call. Providers that accept the "proxy city / session id" encoded in the username make this easy.

import random
import requests

def gateway(country=None, session=None):
    parts = ["user-XXX"]
    if country:
        parts.append(f"-COUNTRY-{country}")
    # A random session id forces a new exit IP on many gateway providers.
    parts.append(f"-SESSION-{random.randint(1, 10_000_000):08d}")
    return "http://" + "".join(parts) + ":PASSWORD@gw.provider.example:8000"


def get(url, country=None, **kw):
    gw = gateway(country=country)
    return requests.get(url, proxies={"http": gw, "https": gw}, timeout=20, **kw)
Enter fullscreen mode Exit fullscreen mode

When it's right: stateless crawls where every page should look like a new visitor. When it breaks: you can't hold a session across calls, because each request hops IPs. Login flows, CSRF token chains, and cart-building all shatter. And you're constructing a string per call — no central place to add health tracking or retries.

Strategy 2: A sticky requests.Session (right for flows)

A Session reuses the underlying TCP connection and, crucially, cookies. Point the session at one gateway with a fixed session id and every request in that session exits the same residential IP — which is exactly what a multi-step flow needs.

import requests


def sticky_session(country="US"):
    s = requests.Session()
    gw = (
        f"http://user-XXX-COUNTRY-{country}"
        f"-SESSION-abc123:PASSWORD@gw.provider.example:8000"
    )
    s.proxies = {"http": gw, "https": gw}
    s.headers.update({"User-Agent": "collection-bot/1.0 (+contact@example.com)"})
    return s


def login_then_scrape():
    s = sticky_session("US")
    s.get("https://shop.example/csrf")          # cookie jar now holds the token
    s.post("https://shop.example/login", data={"u": "...", "p": "..."})
    return s.get("https://shop.example/account")  # same IP, still logged in
Enter fullscreen mode Exit fullscreen mode

When it's right: anything requiring continuity — one identity, one IP, one cookie jar. The scaling question: one session = one account = one IP. To parallelize across 20 accounts, you need 20 independent Session objects, each with its own sticky gateway and cookie jar. Don't share a session across accounts; don't share cookies across sessions. That association discipline is the whole game for multi-account work.

Strategy 3: A custom HTTPAdapter (the pro move)

Both strategies above scatter proxy logic through your calling code. The clean design mounts a ProxyRotator onto the Session as an adapter, so callers just do session.get(url) and never think about gateways, retries, or per-request rotation again. requests already routes every call through HTTPAdapter.send, which makes it the ideal seam.

import itertools
import requests
from requests.adapters import HTTPAdapter


class ProxyRotator:
    def __init__(self, gateways):
        self._pool = itertools.cycle(gateways)

    def next(self):
        return next(self._pool)


class RotatingProxyAdapter(HTTPAdapter):
    """Rotates the exit IP per request and retries on gateway trouble."""

    def __init__(self, gateways, max_retries=3, timeout=20, **kw):
        self.rotator = ProxyRotator(gateways)
        self.max_retries = max_retries
        self.timeout = timeout
        super().__init__(**kw)

    def send(self, request, **kw):
        kw.setdefault("timeout", self.timeout)
        last_exc = None
        for _ in range(self.max_retries):
            gw = self.rotator.next()
            request.url = request.url  # keep as-is; set proxy on send
            kw["proxies"] = {"http": gw, "https": gw}
            try:
                resp = super().send(request, **kw)
                # Treat 407 (proxy auth) as a gateway failure worth rotating.
                if resp.status_code == 407:
                    continue
                return resp
            except (requests.ConnectionError, requests.Timeout) as e:
                last_exc = e  # swap gateway and retry
        if last_exc:
            raise last_exc
        return resp


def build_session(gateways):
    s = requests.Session()
    adapter = RotatingProxyAdapter(gateways, max_retries=3)
    s.mount("https://", adapter)   # mount for your target scheme(s)
    s.mount("http://", adapter)
    return s


if __name__ == "__main__":
    gates = [
        "http://user-x-sess-1:pw@gw1.example:8000",
        "http://user-x-sess-2:pw@gw2.example:8000",
        "http://user-x-sess-3:pw@gw3.example:8000",
    ]
    s = build_session(gates)
    for _ in range(5):
        r = s.get("https://httpbin.org/ip", timeout=20)
        print("origin sees:", r.json()["origin"])
Enter fullscreen mode Exit fullscreen mode

Now your scraping code is beautifully boring: session.get(url). Rotation, retry, and proxy-auth handling all live in one testable class. When it's right: production pipelines where you want proxy policy centralized and swappable — and where you need retry-on-a-different-gateway, which the built-in urllib3.Retry can't do because it retries against the same proxy, not a fresh one.

Choosing, in one line each

  • Per-request proxies= → stateless crawls, fastest to write, no continuity.
  • Sticky Session → multi-step flows and per-account isolation; one identity, one IP, one cookie jar.
  • Custom adapter → production rotation + per-gateway retries with zero proxy logic in your calling code.

The failure mode I see most: people pick per-request rotation (correct for stateless crawling) and then paste it into a login flow, silently breaking every session-continuity assumption. The proxy integration is the architecture decision — rotation versus stickiness — not a config detail.

Two integration gotchas worth pre-empting

Proxy env vars leak in. If the host machine has HTTP_PROXY/HTTPS_PROXY set, requests picks them up via trust_env and they can override or fight your explicit gateways. For predictable behavior set session.trust_env = False when you want only your configured proxies, and session.proxies = {} if you want direct.

SSL verification through MITM gateways. Some residential gateways terminate TLS. If you start seeing certificate errors, don't reach for verify=False reflexively — that silently disables a real security check for your data. Confirm with your provider whether interception is expected; if it is, verify the chain against the provider's CA bundle rather than turning verification off globally.

The takeaway

requests gives you three clean integration points for a rotating pool, and the right one depends entirely on whether your job needs continuity. Reach for a custom HTTPAdapter the moment your pipeline grows real retry logic — it lets your scraper code forget proxies exist, which is exactly what a well-integrated layer should do. Get rotation-versus-stickiness right first; everything else is plumbing.

Disclosure: I use Thordata's residential proxies for this project. New users get 500MB free — code thor020 (10% off): https://www.thordata.com/?ls=uXcSHJzx&lk=02-tele

Top comments (0)