JustProxies

Patterns

IP rotation strategies

4-min readip-rotation
Three rotation granularities — per-request, per-session, and per-domain — each with different trade-offs between anonymity, success rate, and session continuity. Choosing wrong costs you either blocked requests or broken sessions.

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.
per-request.pypython
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.
per-session.pypython
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.
per-domain.pypython
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)
Per-domain pinning only works until the session expires (10–60 minutes of inactivity). After expiry the gateway assigns a new IP. Add a health-check request to renew the token before it expires if you need longer continuity.

Trade-off summary

StrategyIP diversitySession continuityUse case
Per-requestMaximumNoneStateless bulk scraping
Per-sessionOne IP per workflowFullLogin, cart, multi-step
Per-domainOne IP per targetWithin session lifetimeTarget-specific rate budget
HybridHigh (rotating probe, then sticky)After first passBot-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.

worker-pool.pypython
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.

Found a gap, or something wrong?
A real human reads support email.