Rotating Proxy Strategies (Per-Request, Per-Session, Sticky)
Three rotation strategies and when to use each. The mismatch between rotation and session state is the #1 source of bans.
What you’ll learn
- Pick per-request, per-session, or sticky rotation for a given target.
- Implement each in Python and PHP.
- Recognize when rotation breaks logged-in flows.
Most providers expose rotation as a configuration knob. Three modes cover ~95% of use cases. Picking wrong is one of the easiest ways to get banned.
The three modes
| Mode | What happens | Best for |
|---|---|---|
| Per-request | New IP every request | Anonymous browsing, listing pages, no state |
| Per-session (sticky for N minutes) | Same IP for the duration of a logical session | Logged-in flows, multi-step navigation |
| Sticky / dedicated | Same IP for hours/days | Account-bound scraping, captcha-walled sessions |
Per-request rotation
The default for most residential providers. Every request goes through a different IP.
import httpx
client = httpx.Client(
proxies="http://user:pass@gw.provider.com:7777" # rotating gateway
)
for url in urls:
r = client.get(url) # each request gets a fresh IP automatically
When this works:
- Anonymous public pages (listing, search, product detail).
- The target doesn't track state across requests.
- High volume, broad coverage.
When it fails:
- Logged-in scrapes, the session cookie says "this is user X" but the IP changes constantly. Most sites flag this as suspicious.
- Multi-step forms, CSRF token issued on IP A; submit from IP B; rejection.
- Cart/checkout flows, every step looks like a different user.
Per-session rotation
Same IP held for a configurable window (often 10–30 minutes), then released. Providers expose this as a "session" parameter in the proxy URL:
# Smartproxy-style sticky session
client = httpx.Client(
proxies=f"http://user-session-abc123:pass@gw.provider.com:7777"
)
The session-abc123 token tells the gateway "give me the same IP for this session ID." Different session IDs get different IPs.
# Python: one session per worker
session_id = f"worker-{os.getpid()}"
proxy_url = f"http://user-session-{session_id}:{password}@gw.provider.com:7777"
When this works:
- Logged-in scrapes (one session = one IP for the entire login flow).
- Pagination through a search result set (same IP keeps "context").
- Cart/checkout/account flows.
Coordinating session with cookies: every session ID should map 1:1 to a cookie jar. If you reuse a cookie jar but change the session ID (= IP), the target sees "same user, different IPs", back to the per-request problem.
Sticky / dedicated IPs
Some providers offer truly dedicated IPs, yours for the month. Common with ISP proxies and dedicated datacenter.
http://user:pass@123.45.67.89:7777 # always this IP
When this works:
- Accounts that the target whitelists or whose history matters (e.g. you've built reputation on this IP).
- Slow scrapes (one request every few minutes) where a single IP can't trigger volume bans.
- Compliance scenarios where the target needs to know which IP belongs to you.
Risks: if the IP gets banned, your access stops. With rotating, a ban just means one IP out of millions retired.
Strategy by target type
| Target | Strategy |
|---|---|
| Anonymous catalogue scrape | Per-request |
| Search results | Per-session (one session per query thread) |
| Logged-in dashboard | Per-session, locked to the same cookie jar |
| Account-bound API with key | Sticky (one dedicated IP per account) |
| Crawling 100 different domains | Per-request, randomized country |
Implementing in code
Python with Scrapy
class ProxySessionMiddleware:
"""Map cookiejar -> session ID -> stable IP."""
def __init__(self):
self.proxy_template = "http://user-session-{sid}:pass@gw.provider.com:7777"
def process_request(self, request, spider):
cookiejar = request.meta.get("cookiejar", "default")
sid = f"jar-{cookiejar}"
request.meta["proxy"] = self.proxy_template.format(sid=sid)
One cookie jar = one session ID = one IP. Coherent.
Python with httpx
class StickyClient:
def __init__(self, base_proxy, password):
self.clients = {}
self.base = base_proxy
self.pw = password
def for_session(self, session_id):
if session_id not in self.clients:
proxy = f"http://user-session-{session_id}:{self.pw}@{self.base}"
self.clients[session_id] = httpx.Client(proxies=proxy)
return self.clients[session_id]
sticky = StickyClient("gw.provider.com:7777", "PASSWORD")
client_a = sticky.for_session("user-a")
client_b = sticky.for_session("user-b")
# client_a always gets the same IP; client_b a different consistent IP
PHP / Symfony
$client = HttpClient::create([
'proxy' => "http://user-session-$sessionId:$password@gw.provider.com:7777",
]);
For scoped per-session clients, instantiate one HttpClient per session ID. Each gets its own connection pool and IP.
Common mistakes
Rotating IPs across cookie state
Symptom: logged-in scrape works for 5 requests then logs out. Cause: cookie says "user X" but IP keeps changing. Fix: use per-session rotation, pin one IP per session.
Sharing sessions across threads
If two threads use the same session ID, they share the IP, fine. But if they also share the cookie jar and one logs out, both lose the session. Either: separate session per thread, or careful coordination of shared state.
Forgetting to rotate the session ID
After enough time on one IP, you accumulate enough request history to look bot-like even on a residential IP. Rotate session IDs every N requests or N minutes to refresh.
# Rotate session every 100 requests
counter = 0
session_id = f"jar-{cookiejar}-{counter // 100}"
Geographic stickiness
When using per-request rotation, you may inadvertently bounce between countries. Most providers let you constrain region:
http://user-country-de-session-abc:pass@gw.provider.com:7777
All Germany-based IPs, all session abc. Bouncing countries mid-scrape can break geo-locked content or trigger anti-fraud rules.
Costs
Per-request rotation usually has no premium. Per-session is sometimes free, sometimes premium. Sticky/dedicated almost always premium. Match the strategy to actual need; don't pay for stickiness when per-request would do.
Hands-on lab
Pick a target with login:
- Implement a logged-in scrape with per-request rotation. Watch it fail or log out partway through.
- Switch to per-session rotation, mapping session ID to cookie jar. Watch it succeed.
- Add session-ID rotation every 50 requests. Confirm continued success.
The lesson lands when you've seen rotation work and fail. After that, picking the strategy becomes intuitive.
Quiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.