The choice between rotating vs sticky proxies comes down to whether the target remembers you between requests. A rotating proxy sends each request, or each new connection, from a different exit IP, which spreads load and suits independent, stateless fetches. A sticky proxy holds one exit IP for a session, which is required whenever the target ties state to your address: logins, carts, paginated result sets and multi-step forms. Most production systems use both, choosing per flow rather than per project.
This guide covers how each mode behaves, how long a sticky session should last, what to do when one ends early, and three patterns for managing sessions in code.
How rotating and sticky sessions differ
The practical difference in rotating vs sticky proxies is what the target can link together. With per-request rotation, the gateway picks a new exit address for each request from the pool that matches your targeting. Two consecutive requests to the same site will usually arrive from unrelated addresses. Nothing links them from the target's point of view except what your client sends: cookies, headers and the TLS fingerprint.
With a sticky session, the gateway pins your traffic to one exit address for a period of time. On a peer-based network, the address belongs to a real residential or mobile connection, and the session lasts until its configured duration expires or until the peer goes away, whichever comes first.
On ProxyForge, you choose the session mode, along with the exit country, in the dashboard's configuration panel, which generates the exact connection string for that combination. Residential sessions can be sticky for up to 60 minutes, and mobile sessions for up to 30 minutes. ISP and datacenter addresses are static and do not rotate at all.
| Per-request rotation | Sticky session | Static ISP or datacenter | |
|---|---|---|---|
| Exit IP between requests | Changes | Held for the session | Never changes |
| Maximum duration | Not applicable | Up to 60 minutes residential, up to 30 minutes mobile | As long as you hold the address |
| Can end early | Not applicable | Yes, if the peer goes offline | No, barring an outage |
| Server-side state survives | No | Yes, within the session | Yes |
| Load spread across addresses | Automatic | Across sessions you open | Across addresses you hold |
| Best for | Independent page fetches, SERP checks, price snapshots | Logins, carts, pagination, multi-step forms | Long-lived accounts, allowlisted partners, persistent identities |
Which flows need a sticky session?
A flow needs stickiness when the target stores something about you on its side and checks it against your address. The common cases:
Pagination with server-side state. Some sites return a cursor or a search context that is bound to the session that created it. Requesting page two from a new address can return page one, an empty result, or an error. Stateless pagination, where the page number is in the URL and any client can request any page, does not need stickiness.
Carts and checkouts. A cart stored in a server-side session keyed by a cookie is often validated against the address it was created from. A cart that changes address mid-flow looks like session hijacking to fraud tooling.
Logins. Many sites bind an authenticated session to the login IP, or re-challenge when the address changes. Any flow that logs in and then reads account pages needs one address for the whole visit.
Multi-step forms. Search forms with a CSRF token, quote builders and wizards carry state from step to step, and some reject a submission from a different address than the one that fetched the form.
Anything behind a challenge you have passed. If a target sets a clearance cookie after a check, that cookie is often only valid from the address that earned it.
A useful test: if you can describe the flow as "fetch these URLs in any order, from anywhere", rotate. If you have to say "first, then, then", stick.
Identity is more than the IP
A sticky session only helps if everything else about the client stays consistent too. Keep one cookie jar, one set of headers and one client per session, and discard all of them together when the session ends. A new address carrying an old cookie is as suspicious as an old address with a new one. The reverse also holds for rotation: rotating the IP while sending the same session cookie on every request defeats the purpose.
How long should a sticky session last?
Choose the shortest duration that comfortably covers one complete flow. Longer is not safer.
- Too short and the session expires mid-flow. The next request leaves from a new address, and the target sees a login or cart jump addresses halfway through.
- Too long and a single address carries more of your traffic than a real user would generate, which raises its chance of being rate limited or flagged. A long session also concentrates risk: if the address is blocked, everything pinned to it stops.
Measure how long your flow takes at the 95th percentile, add a margin, and set the session length from that. If your longest flow runs longer than the maximum session length for the line you are using, split it into resumable stages, or move that flow to a static line.
Design for sessions that end early
On residential and mobile lines, the exit address is a real device on a real network. Phones change cells, laptops close, home routers restart. A sticky session will sometimes end before its configured duration, and your code has to treat that as normal rather than exceptional.
In practice, that means:
- Make flows restartable from the beginning. Do not try to continue a login or cart from a new address; start the flow again with a fresh session, fresh cookies and a fresh client.
- Detect the break. A sudden connection error, a redirect to a login page, or a changed exit address all indicate the session is gone. For critical flows, record the exit address at the start from an IP echo service and check it again before the step that matters.
- Bound retries. Restart a flow a small number of times with new sessions, then give up and record the failure rather than burning bandwidth.
When you need an address that never changes
If a flow needs the same address for hours or days, such as a long-lived account, a partner that allowlists your IP, or a monitoring job that must look consistent over time, a sticky session on a peer network is the wrong tool. Static ISP and datacenter addresses are, in effect, permanently sticky: each address is dedicated to you and stays yours until you release it. ISP addresses are leased from consumer ISPs, so they tend to be treated like home connections by targets that score IP reputation; datacenter addresses come from registered ranges and suit targets that tolerate them. The residential vs ISP vs datacenter comparison covers the full trade-off, and the ISP proxies page lists what is available.
Patterns for managing sticky sessions
How you assign sessions to work determines how well the system survives sessions ending. Three patterns cover most systems.
Session per worker
Each worker process or thread gets one sticky session at startup and uses it for everything it does. This is the simplest pattern and fits a small number of long-running workers doing similar, low-state work.
Its weakness is coupling: if the session dies, the worker's current task fails with it, and a worker that holds one address for hours sends a concentrated stream of traffic from that address. It also ties the number of addresses in use to the number of workers, which is rarely the right number.
Session per task
Each logical task, such as one login-and-scrape, one checkout test or one paginated search, gets its own session. When the task finishes, the session is released. When the session fails, the task restarts with a new one.
This matches stickiness to the unit of state, which is usually what you want. It needs a way to hand a session to a task and take it back, which leads to the third pattern.
Session pools with health scoring
Keep a pool of sticky connection strings, assign one per task, and score each on outcomes. Sessions that keep failing are rested for a cooldown and then retried, and tasks are always given the healthiest free session. This absorbs early session ends without manual intervention and stops the system from repeatedly assigning work to an address a target has started blocking.
The sketch below implements this. It reads a list of sticky connection strings, each generated in the dashboard for the country and session mode you need, from one environment variable, one per line. It uses Python Requests and passes proxies per request, which takes precedence over proxy environment variables.
import os
import threading
import time
from dataclasses import dataclass
import requests
@dataclass
class ProxySession:
proxy_url: str
successes: int = 0
failures: int = 0
cooldown_until: float = 0.0
@property
def score(self) -> float:
return (self.successes + 1) / (self.successes + self.failures + 2)
class StickySessionPool:
def __init__(self, proxy_urls, min_score=0.5, cooldown_seconds=300):
self._sessions = [ProxySession(url) for url in proxy_urls]
self._by_task: dict[str, ProxySession] = {}
self._lock = threading.Lock()
self._min_score = min_score
self._cooldown_seconds = cooldown_seconds
@classmethod
def from_env(cls, name="PROXY_STICKY_URLS"):
urls = [line.strip() for line in os.environ[name].splitlines() if line.strip()]
return cls(urls)
def _usable(self, session):
return session.cooldown_until <= time.monotonic() and session.score >= self._min_score
def acquire(self, task_id):
with self._lock:
current = self._by_task.get(task_id)
if current and self._usable(current):
return current
in_use = {id(s) for s in self._by_task.values()}
free = [s for s in self._sessions if id(s) not in in_use and self._usable(s)]
if not free:
raise RuntimeError("no healthy sticky session is free")
chosen = max(free, key=lambda s: s.score)
self._by_task[task_id] = chosen
return chosen
def release(self, task_id, ok):
with self._lock:
session = self._by_task.pop(task_id, None)
if session is None:
return
if ok:
session.successes += 1
else:
session.failures += 1
session.cooldown_until = time.monotonic() + self._cooldown_seconds
def run_flow(pool, task_id, urls):
session = pool.acquire(task_id)
proxies = {"http": session.proxy_url, "https": session.proxy_url}
try:
with requests.Session() as http:
for url in urls:
response = http.get(url, proxies=proxies, timeout=30)
response.raise_for_status()
except requests.RequestException:
pool.release(task_id, ok=False)
raise
pool.release(task_id, ok=True)
def run_with_restarts(pool, task_id, urls, attempts=3):
for attempt in range(1, attempts + 1):
try:
return run_flow(pool, task_id, urls)
except requests.RequestException:
if attempt == attempts:
raise
A few details are deliberate. Each flow gets a new requests.Session, so cookies never outlive the address they were set from. A failed flow puts its session on cooldown, so the restart in run_with_restarts receives a different one. The score is a smoothed success rate, which keeps a single early failure from condemning a new session while still pushing persistently failing ones below the threshold. A production version would also detect soft failures, such as a login page returned with a 200, and treat them as failures; handling proxy 403 and 429 errors covers how to recognize those.
For rotating traffic, none of this machinery is needed: use a single rotating connection string and let the gateway choose the address. In a crawler framework, the Scrapy proxy guide shows how to wire both modes into middleware.
Rotating vs sticky proxies: a checklist for each flow
The decision between rotating vs sticky proxies is made per flow, not per project. Run each flow in your system through these questions:
- Does the target keep state for this flow on its side, such as a login, cart, cursor or form token? If yes, use a sticky session.
- Does the flow finish comfortably inside the line's maximum session length? If not, split it into resumable stages or use a static address.
- Can the flow restart cleanly from the beginning if the session ends? If not, make it restartable before relying on a peer-based sticky session.
- Is each request independent of the others? If yes, rotate per request.
- Does a partner allowlist your address, or does an account need the same address for days? If yes, use a static ISP or datacenter address.
- Are cookies, headers and client fingerprint scoped to the same unit as the IP? If not, fix that first.
How ProxyForge handles sessions
On ProxyForge, residential proxies rotate per request or hold a sticky session for up to 60 minutes, mobile proxies rotate or hold for up to 30 minutes, and ISP and datacenter addresses are static and dedicated to one customer. The session mode and exit country are selected in the dashboard, which generates the connection string, so a pool like the one above is filled from the dashboard rather than from hand-built strings.
Residential and mobile are billed per GB and static lines per address per month, so a system that mixes rotating, sticky and static traffic pays for each in the unit that suits it. Current rates are on the pricing page.