Per-request rotation
The default. No session token in the username — every request goes through a different exit IP. This is the most anonymous mode and the most resilient to per-IP rate limits.
- Best for: stateless pages — product listings, SERP pages, public profiles, price data. Any job where each request is independent.
- Not good for: anything that depends on session continuity. Cookie-based sessions break if the IP changes between requests.
- Retry behaviour: each retry naturally lands on a different IP, so transient blocks clear automatically.
import requests
# No session token — new IP per request.
proxy = "http://USER:[email protected]:8080"
proxies = {"http": proxy, "https": proxy}
for url in urls:
r = requests.get(url, proxies=proxies, timeout=20)
process(r)
Per-session (sticky)
One session token per logical user or workflow. All requests sharing the token exit through the same IP for the session lifetime (up to 60 minutes).
- Best for: login flows, shopping carts, multi-step forms, anything where the target IP-pins a cookie or CSRF token.
- Not good for: bulk parallel scraping — holding many sticky sessions simultaneously reduces IP diversity across your fleet.
import secrets, requests
def make_session(user, password):
token = secrets.token_hex(8)
proxy = f"http://{user}-session-{token}:{password}@gw.justproxies.online:8080"
s = requests.Session()
s.proxies = {"http": proxy, "https": proxy}
return s
# Each account gets its own sticky session.
for account in accounts:
sess = make_session("USER", "PASS")
sess.post("https://target.com/login", data=account)
yield from scrape_account(sess)
See sticky sessions for the token syntax and lifetime details.
Per-domain pinning
Use one sticky token per target domain across all workers. Every worker hitting site-a.com uses the same IP; every worker hitting site-b.com uses a different one. This is useful when:
- A target allows a certain number of requests per IP per hour and you want to spend that budget from one IP rather than spreading thin.
- The target's CDN assigns server-side state to the first IP in a session and re-routing to a different IP triggers a re-auth or cache miss.
from urllib.parse import urlparse
import requests
# One fixed token per domain — renewed when the session expires.
DOMAIN_TOKENS: dict[str, str] = {}
def get_proxy_for(url: str) -> dict:
domain = urlparse(url).netloc
if domain not in DOMAIN_TOKENS:
import secrets
DOMAIN_TOKENS[domain] = secrets.token_hex(8)
token = DOMAIN_TOKENS[domain]
p = f"http://USER-session-{token}:[email protected]:8080"
return {"http": p, "https": p}
for url in urls:
r = requests.get(url, proxies=get_proxy_for(url), timeout=20)
Trade-off summary
| Strategy | IP diversity | Session continuity | Use case |
|---|---|---|---|
| Per-request | Maximum | None | Stateless bulk scraping |
| Per-session | One IP per workflow | Full | Login, cart, multi-step |
| Per-domain | One IP per target | Within session lifetime | Target-specific rate budget |
| Hybrid | High (rotating probe, then sticky) | After first pass | Bot-check then transact |
Practical patterns
Parallel workers with per-worker sticky sessions — each worker holds one token for its lifetime. IP diversity scales with worker count; each worker has full session continuity within its own scope.
import secrets
from concurrent.futures import ThreadPoolExecutor
import requests
def worker(task_urls):
token = secrets.token_hex(8)
p = f"http://USER-session-{token}:[email protected]:8080"
s = requests.Session()
s.proxies = {"http": p, "https": p}
for url in task_urls:
yield s.get(url, timeout=20)
# 16 workers, each with its own IP — 16 different exit IPs in parallel.
chunks = [urls[i::16] for i in range(16)]
with ThreadPoolExecutor(max_workers=16) as ex:
list(ex.map(lambda c: list(worker(c)), chunks))
The rotating vs sticky article covers the hybrid fingerprint-then-hold pattern in detail.