Skip to content
Proxy fundamentals

Rotating vs sticky sessions: choosing how addresses change

When to rotate on every request, when to hold an address, and why rotating too aggressively looks more suspicious.

Every rotating proxy service offers two modes: change the address constantly, or hold one for a while. Choosing wrongly is one of the more common causes of unexplained failure, and the instinct most people bring — more rotation is safer — is frequently backwards.

#The two modes

Per-request rotation Sticky session
Address changes Potentially every request Held for a set window
Suits Independent fetches Multi-step flows
Cookies and login Break Survive
Pool exposure Spread thin Concentrated

#When per-request rotation is right

Use it when each request stands alone: fetching a list of unrelated product pages, checking availability across many URLs, sampling search results from many regions. Nothing carries state between requests, so nothing breaks when the address changes.

The benefit is that no single address accumulates enough requests to look unusual. The load is spread across the pool.

#When it actively hurts

Consider a realistic sequence: load a search page, submit a query, open result three, then page to the next set. Four requests that a server expects from one person.

With per-request rotation those arrive from four different addresses — plausibly four countries — inside a few seconds, carrying the same session cookie. No genuine user does that. You have handed the destination an unambiguous automation signal that would not exist if you had simply held one address.

Anything with a login, a basket, a multi-page form or a server-side session needs stickiness. So does anything where the site issues a token tied to the connection.

#How to request it

Most providers accept a session identifier inside the proxy username. A representative shape:

curl -x "http://user-country-de-session-abc123:[email protected]:8000" \
     https://example.com/step-one

# Same identifier, same exit address
curl -x "http://user-country-de-session-abc123:[email protected]:8000" \
     https://example.com/step-two

There is no standard here. Separators, field names and ordering all differ between providers, and a malformed field usually surfaces as a 407 rather than a helpful error. Confirm the syntax before building against it.

#Stickiness is best-effort

This is the detail that produces intermittent bugs months later. A sticky window is an intention, not a guarantee. If the underlying address leaves the pool — a residential peer closes their laptop, for example — your session ends early and the next request exits somewhere else.

Write code that notices:

import httpx

def exit_ip(client):
    return client.get("https://api.ipify.org").text.strip()

with httpx.Client(proxy=PROXY, timeout=20) as c:
    start = exit_ip(c)
    for step in flow:
        r = c.get(step)
        if exit_ip(c) != start:
            restart_flow()      # address moved; the session is gone
            break

Checking the address on every step costs a request each time, so in production most people check only at the points where a broken session would be expensive.

#Choosing a window length

Long enough to finish the task, and no longer. An address held far past the work it was doing accumulates request history against a single exit for no benefit. If your flow takes forty seconds, a one-minute window is right; a thirty-minute window simply concentrates exposure.

#What rotation does not solve

  • Rate limits tied to an account. If you are logged in, the limit follows the account, not the address.
  • TLS and browser fingerprints. Identical across every address you use.
  • Cookies you keep sending. Rotating address while replaying the same session cookie is a contradiction the server can see plainly.

Rotation is a tool for distributing load and avoiding per-address thresholds. It is not a disguise, and treating it as one leads to spending more on proxies to fix problems that live somewhere else entirely.

#Matching rotation to the task

A short table settles most cases without further thought:

Task Mode Reason
Fetching many unrelated pages Per request No state to preserve; spreads load
Paging through results Sticky The server treats paging as one session
Anything behind a login Sticky Address change invalidates plausibility
Checkout or multi-step forms Sticky Tokens are frequently bound to the connection
Price checks across countries Per request, geo-targeted Each check is independent
Monitoring one URL repeatedly Per request Avoids one address accumulating history

#What “one session” means to the server

It helps to think about what the destination is correlating. Typically at least four things: the source address, the session cookie, the TLS fingerprint, and behavioural timing. A consistent session means all of them stay coherent together.

Rotating address while replaying the same cookie is the most common incoherence, and it is trivially detectable: the server sees one identity arriving from three countries in ten seconds. If you rotate, rotate the cookie jar too, and accept that you are starting a new identity rather than continuing one.

#Detecting a session that ended early

Because stickiness is best-effort, code that assumes a stable address will eventually be wrong. Detect it rather than hope:

import httpx

class StickyFlow:
    def __init__(self, proxy):
        self.client = httpx.Client(proxy=proxy, timeout=30, follow_redirects=True)
        self.anchor = self._exit()

    def _exit(self):
        return self.client.get("https://api.ipify.org").text.strip()

    def get(self, url, verify_every=3):
        self._n = getattr(self, "_n", 0) + 1
        r = self.client.get(url)
        if self._n % verify_every == 0 and self._exit() != self.anchor:
            raise RuntimeError("exit address changed; session is gone")
        return r

Checking every request doubles your request count, so check periodically, and always at the points where a broken session would be expensive — immediately before a submission, for example.

#Choosing a window length

Long enough to complete the work, no longer. The reasoning is simple: while an address is held, every request adds to that single exit‘s recent history at the destination. A window far longer than the task concentrates exposure for no benefit.

  • Under a minute — a short flow of a few requests.
  • A few minutes — login plus a handful of authenticated pages.
  • Ten minutes or more — long interactive sessions, and worth questioning whether the work could be split.

#Rotation is not a reset

This is worth stating plainly because it drives a lot of wasted spend. A new address does not clear:

  • Account-level rate limits. If you are authenticated, the limit follows the account.
  • Fingerprints. Your TLS and browser signatures are identical from every address you own.
  • Cookies you keep sending. You are carrying the old identity with you.
  • Behavioural patterns. Perfectly regular request intervals look mechanical regardless of origin.

If rotating faster is not helping, that is evidence the constraint is one of these, and buying a larger pool will not move it.

#Pool exposure, and why it is a real cost

Every request through an address contributes to how that address is perceived at the destination. On a shared residential pool you are not the only tenant — other customers’ behaviour affects the same addresses.

Two practical consequences. Rotating aggressively against a single target spreads your footprint across many addresses rather than concentrating it, which is usually good. But rotating aggressively while behaving obviously like automation degrades many addresses instead of one, which is usually bad. The mode you choose interacts with how well-behaved your requests are; neither decision is independent of the other.

#A checklist before you go to production

  1. Does the task carry state between requests? If yes, sticky.
  2. Is the session identifier syntax confirmed against the provider’s documentation?
  3. Does the code detect an early session end rather than assume it cannot happen?
  4. Is the cookie jar scoped to the same lifetime as the address?
  5. Is request pacing irregular enough to not look generated?
  6. Are you logging which exit address served each request, so failures can be traced?

#A worked example: paging through results

Consider fetching five pages of search results. Both approaches are shown so the difference is concrete:

import httpx, uuid

# Wrong for this task: a new address may serve each page
def paged_rotating(query, pages=5):
    out = []
    for p in range(1, pages + 1):
        with httpx.Client(proxy=ROTATING_PROXY, timeout=30) as c:
            out.append(c.get(f"https://example.com/search?q={query}&page={p}"))
    return out

# Right: one address, one cookie jar, for the whole sequence
def paged_sticky(query, pages=5):
    session = f"s-{uuid.uuid4().hex[:8]}"
    proxy = f"http://user-session-{session}:[email protected]:8000"
    with httpx.Client(proxy=proxy, timeout=30, follow_redirects=True) as c:
        return [c.get(f"https://example.com/search?q={query}&page={p}")
                for p in range(1, pages + 1)]

The first version also creates a fresh client per page, so it pays a new TLS handshake every time. The second reuses one connection and one identity. On a per-gigabyte plan the difference shows up on the invoice as well as in the block rate.

#When both are wrong

Sometimes the honest answer is that rotation is not your variable at all. If a target refuses you on the very first request, before any pattern could have formed, then nothing about rotation is relevant — you are being identified on the handshake or on missing JavaScript. Changing rotation strategy in that situation produces no improvement and consumes a day finding that out.

A quick discriminator: does a single, first, clean request succeed? If yes, your problem is accumulation and rotation matters. If no, your problem is identity and rotation is irrelevant.

#Interaction with rate limits

Rotation and pacing pull in different directions and are easy to confuse. Rotation spreads requests across addresses so no single address crosses a per-address threshold. Pacing reduces the total rate. If the limit being enforced is per account or per fingerprint, rotation does nothing and only pacing helps — which is exactly the situation described in reading proxy and block errors.

The practical rule: if you are seeing 429s, change pacing first and rotation second. If you are seeing 403s after a period of success, consider rotation. If you are seeing 403s immediately, look at your client.

Frequently asked questions

Is rotating on every request safer?
Not inherently. For independent single-request fetches it distributes load well. For a multi-step flow it is worse, because a real user's address does not change between clicks. Match rotation to the task.
How long can a sticky session last?
It varies by provider, typically from about one minute to around thirty. The window is a maximum, not a guarantee: if the underlying address leaves the pool the session ends early.
How do I request a sticky session?
Most providers accept a session identifier inside the proxy username, such as a field like session-abc123. Requests carrying the same identifier route through the same address. The exact syntax is provider-specific.
Does rotating my IP reset a rate limit?
Often not. Many services apply limits per account, per session cookie or per fingerprint rather than per address. If you are rate limited while logged in, a new address changes nothing.

Sources

  1. RFC 6265: HTTP State Management (cookies)
  2. RFC 9110: HTTP Semantics

Read this page as Markdown · Quote it freely under CC BY 4.0 with a link back.