Skip to content

Sticky Sessions and Rotation

peruvian edited this page Jul 2, 2026 · 5 revisions

Sticky Sessions and Rotation

The single most misunderstood part of mobile proxies. Read this before answering your first "the IP keeps changing" support ticket.

There are two completely separate layers of IP change at play. Mixing them up causes every conversation about sticky sessions to go in circles. This page draws the line clearly.


The two layers

┌────────────────────────────────────────────────────────────────┐
│ LAYER 1 — Gateway routing                                       │
│ "Which modem does my request go to?"                            │
│ Controlled by: rotation token (sticky / auto5 / auto20 / hard) │
│ Controlled by: us                                               │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ LAYER 2 — Carrier / ISP                                         │
│ "What public IP does that modem expose right now?"              │
│ Controlled by: T-Mobile / Verizon / EE / etc. (carrier CGNAT)  │
│ Controlled by: NOT us                                           │
└────────────────────────────────────────────────────────────────┘

When a customer says "sticky isn't sticky, the IP changed", what they almost always mean is Layer 2 changed. Layer 1 did its job — they kept talking to the same modem the whole time — but the carrier rotated that modem's public IP out from under us.


Layer 1 — Gateway rotation tokens

The username DSL has these rotation modes:

Token What the gateway does
rot-sticky Pin every request with the same sid to the same modem for the session
rot-sticky-strict Like sticky, but on failover re-pins only within the same node instead of re-picking freely (SDK 0.7.0+)
rot-auto5 / auto10 / auto20 / auto60 Pin to one modem for N minutes, then soft-rotate you to a new one. auto10 is the default when no -rot- token is given (auto30 is a legacy alias that snaps to auto20)
rot-ondemand New endpoint on a new connection (no auto-interval; reusing the same sid keeps the pin)
rot-hard Pins like sticky. It does NOT pick a new modem per request. (A true carrier-IP reset only happens via the explicit /rotate action, and isn't available for residential peers.)

⚠️ sticky and auto* need a sid. Stickiness lives in the session, and a session is named by its sid. Without a -sid- token, every connection starts a fresh, un-referenceable session — so there is no cross-connection stickiness, even with -rot-sticky. Always pass -sid-<name> when you want a connection to "stick".

The sid-<id> token names your session ("port name"). Different sid values get different modems; the same sid repeated keeps you on the same modem (in sticky/auto modes). Drop the sid and each connection is independent. Format: lowercase letters, digits, and underscores, 1-64 chars - no dashes, since the dash delimits tokens. The SDK's buildProxyUrl validates this since 0.8.1 (short sids like t1 are fine; the old 8-char minimum was removed).

Example:

# Two terminals using the SAME sid will share a modem
curl -x 'http://psx_xxx-mbl-us-sid-job1-rot-sticky:pak_xxx@gw.proxies.sx:7000' https://api.ipify.org
curl -x 'http://psx_xxx-mbl-us-sid-job1-rot-sticky:pak_xxx@gw.proxies.sx:7000' https://api.ipify.org

# A different sid starts on a different modem
curl -x 'http://psx_xxx-mbl-us-sid-job2-rot-sticky:pak_xxx@gw.proxies.sx:7000' https://api.ipify.org

This is the only part we control directly.


Layer 2 — Carrier CGNAT (the part everyone forgets)

Every mobile carrier in the world puts millions of devices behind a shared NAT pool — Carrier-Grade NAT, or CGNAT. Your modem doesn't actually own a unique public IPv4. It borrows one from the carrier's pool. The carrier reassigns IPs out of that pool whenever it wants — sometimes per-second on busy networks, sometimes per-hour on calm ones, sometimes per-TCP-flow.

T-Mobile US, for example, has a ~50K-address /16 pool (172.56.0.0/16 is one of theirs). A single modem sitting still on a desk will see its public IP cycle through dozens of those addresses over a single afternoon. The modem isn't moving. The IP is being recycled around the modem by the carrier.

We can pin you to a modem (Layer 1). We cannot pin the carrier (Layer 2).

Real-world example (5 minutes on a shared mobile sticky session)

t=  0s  172.56.169.18      ←─┐
t= 12s  172.56.168.58       │ Same modem
t= 23s  172.56.168.166      │ (gateway sticky worked)
t= 35s  172.56.168.224      │
…                            │ Carrier rotated the IP
t=275s  172.56.168.148      │ 24+ times during 5 minutes
t=287s  172.56.168.148      │
t=298s  172.56.168.148      │
…                            │
t=344s  172.56.169.44       ←─┘

27 unique IPs in 31 samples. All in the same T-Mobile /23. The session never hopped modems — it just hopped IPs inside the carrier's CGNAT pool. That's normal mobile carrier behavior.


When this matters

Most use cases don't care about per-second IP changes:

  • Scraping where each request is independent → fine, even prefer rotation
  • Account warming on a single service → service tracks you by cookie, not IP
  • Geo-checking where you only need country accuracy → fine
  • General-purpose anti-block where any mobile IP works → fine

A small number of workflows fail catastrophically if the IP shifts even once:

  • cf_clearance cookies (Cloudflare) — the cookie is bound to the IP that solved the challenge. CapSolver needs ~10–30s to solve, then your browser must reload using the same IP, or the cookie is rejected.
  • Banking / fintech sessions — many bind the session token to the source IP and 401 you on any change.
  • Single-IP TLS / mTLS to a peer — if you re-handshake from a different IP, the peer might revoke.
  • TCP-level sticky load balancers on the target side — they fingerprint by IP.

For these, sticky-on-shared-mobile isn't enough. You need an IP-stable solution.


How to actually get IP stability

You have three options, ordered by what most people want first:

Option 1 — Use the peer (residential) pool

http://psx_xxx-peer-us-sid-job1-rot-sticky:pak_xxx@gw.proxies.sx:7000

The peer pool is the community network - largely residential ISP connections (real Comcast / Spectrum / Deutsche Telekom subscribers), with some mobile peers mixed in. Residential ISPs change WAN IPs much less aggressively than mobile carriers — typically hours to days, sometimes never until reboot. Sticky + peer + a session of < 1 hour is IP-stable in practice ~95% of the time.

Trade-off: peer pool quality is more variable than mobile — it's a community network growing daily. Use for IP stability when you can tolerate occasional slower endpoints. Since SDK 0.8.x you can also hard-target a specific carrier inside the pool with the asn / isp options on buildProxyUrl (check availability first with pool.getCarrierStock()).

Option 2 — Dedicated mobile port (for cf_clearance and similar)

Ask us to provision a dedicated modem with rotation disabled at the modem level. This is the only way to get a mobile IP that doesn't shift mid-session. The modem holds a single carrier IP for as long as the carrier allows (often 24h+ on the same TCP flow).

How to request:

  • Email us with your country + carrier preference + expected concurrency + estimated daily GB
  • We provision a modem with rotation=off and assign it a high MAXCONN
  • You get a dedicated host:port:user:pass (not the shared gateway)
  • You charge a higher price to your customer — typically 2–4× the shared rate

This is the solution for cf_clearance, mTLS, and IP-bound banking workflows. There is no cheaper way to get an IP-stable mobile exit.

Option 3 — Catch the IP shift and re-solve

If you're using shared sticky mobile and your workflow can tolerate occasional re-solves (e.g. you have unlimited CapSolver budget), have your code:

  1. Record currentIp after every page load (use https://api.ipify.org)
  2. If currentIp changed since the last load, drop your cookie and re-solve
  3. Otherwise continue

This is operationally fine but expensive. Most resellers prefer Option 2 for any volume workload.


Sticky behavior on the mbl shared pool — what to expect

When a customer asks "should I expect IP stability on sticky shared mobile?":

Time window Typical IP-stable probability
Single request (< 1s) ~100% (IP doesn't shift mid-request)
10 seconds ~50% on T-Mobile, ~80% on Verizon, varies by carrier
60 seconds ~20%
5 minutes < 5%
1 hour ~0% on any major US carrier

These are empirical and carrier-dependent. EU carriers (EE UK, Vodafone DE, Orange FR) tend to hold IPs longer than US carriers in our experience.

If your customer's workflow needs more than ~10 seconds of IP stability, point them at Option 1 or Option 2 above. Don't try to make shared sticky do something it can't.


What if it really isn't sticky at the gateway either?

There is a separate, real bug class where Layer 1 itself fails — the gateway round-robins you across different modems even with rot-sticky. You can detect it by:

  1. Running 30+ requests over 5 minutes with the same sid
  2. Inspecting the IPs (https://api.ipify.org)
  3. Look at the carrier /24 (first 3 octets) — if every IP is in the same /23 or /24, that's one modem with carrier CGNAT (normal). If you see IPs from totally different carrier ranges (different /16s, different ASNs), that's the gateway hopping modems.

If you see modem-hopping (different ASNs / carrier ranges) with rot-sticky, file an issue at the main repo with:

  • The full proxy URL you used (redact the pak)
  • The list of IPs you observed
  • The timestamp range

We treat this as a P0.

What the gateway does internally (May 2026 — #295 fix)

For full transparency, here's what the gateway does on every request that carries a sid:

  1. Look up the pinned modem in Redis.
  2. If the modem is currently online → reuse it. Outcome: reused.
  3. If the modem is currently offline but was last seen within the last 60s and rotation is sticky or hard → reuse it anyway (probe blip tolerance). Outcome: reused_blip.
  4. Else → pick a fresh modem and update the pin. Outcomes: dropped_offline or dropped_missing.

The 60s grace window was added because the gateway's pool-scanner runs */2 minutes and can briefly flip a modem to offline on a single missed probe. Before the grace was added, a customer arriving during that ~1s window would silently lose their sticky pin.

Operators can watch the prom metric gateway_sticky_outcome_total{rotation="sticky",outcome} to see Layer 1 health at fleet scale. If dropped_* ÷ (reused + reused_blip) exceeds ~5% it indicates a real Layer 1 problem worth investigating; below 5% means the gateway is doing its job and any customer complaint about IP changes is Layer 2 (carrier CGNAT — Options 1/2/3 above are the answer).

Sticky smart-selection (May 2026 — #295 Phase 1)

When picking a modem for a sticky/hard session (no existing pin yet), the gateway now biases toward modems whose carrier holds the IP best:

  • The pool-scanner records every observed IP change per modem (sliding 24h sorted set).
  • Each scan, ipStabilityScore (0–100) is computed and written to the modem's Redis hash: 100 = IP held across all polls this hour, 0 = IP changed on every poll.
  • The Lua selector's scoring formula is rotation-aware:
    • For sticky / hard: score = load*0.3 + health*0.2 + stability*0.5
    • For auto* / ondemand: score = load*0.8 + health*0.2 (legacy — soft rotation wants diversity)

A T-Mobile US modem that's been rotating IPs every poll scores ~0 on stability and effectively only gets picked for sticky when nothing better is available. A Verizon line that's held a stable IP for an hour scores ~100 and wins decisively. Customers don't change anything — sticky just routes to better modems automatically.

This is Phase 1 — the signal is coarse (2-min poll cadence means we can't distinguish modems that rotate every 8s from modems that rotate every 30s). Phase 2 will add a higher-frequency IP probe for finer-grained stability scoring. Phase 3 is procuring SIM-side static-IP modems for premium tiers.

Distinct-customer cap on modems (May 2026 — #295 Phase 1 diversity)

Tightened from maxCustomers=3 to maxCustomers=2 per modem (and maxConnections=6 → 3). Why: with the old cap, three different sticky customers could pile up on the same modem and (a) share its single CGNAT IP pool which kills the IP diversity those customers came to mobile proxies for, and (b) compete for the modem's uplink bandwidth. The tighter cap spreads sticky load across more modems so each customer has more of "their own" modem.

Returning customers (same pakKeyId, multiple parallel sockets) bypass the cap — only NEW customers count toward the HLEN check.


Summary

If the IPs are… Then…
Always in the same /24 (same carrier subnet) Sticky is working — carrier is rotating IP inside its CGNAT
Same ASN but different /16s Carrier is using a wider pool — still Layer 2, still normal
Different ASNs (e.g. T-Mobile then AT&T then Verizon) Gateway sticky bug — report it
You need IP stability over > 10s on mobile Use peer pool or dedicated port — not shared sticky

Next reads

  • Pak-Key-Lifecycle — how paks behave on top of sticky sessions
  • Troubleshooting — full error catalog including session edge cases
  • Glossary — formal definitions of sticky/sticky-strict/auto*/hard

Clone this wiki locally