Quick answer
How do sticky sessions and IP rotation work on a residential proxy?
On CrawlRelay, a username without a session ID gets an IP from the pool on every new TCP connection, usually a different one, and -session-<id>-ttl-<minutes> keeps one IP for that ID for up to 30 minutes, though the IP can change earlier. Rotation follows connections, not HTTP requests, so keep-alive decides when your IP really changes. Residential traffic costs $0.50/GB from 50 GB. Paid in crypto, with no KYC.
Session rules at a glance
| Rotation unit | An IP from the pool per new TCP connection, usually a different one |
|---|---|
| Sticky session | -session-<id>-ttl-<minutes> appended to the username |
| Session length | up to 30 minutes |
| Session ID | 1–32 letters or digits; one ID per flow |
| What ends a session early | The residential device behind it going offline |
| Shared IDs | Clients that send the same ID share the same exit |
| Cost | None: sessions and rotation are included in the per-GB price |
What happens at the gateway when I send a session ID?
The gateway reads the username on each new connection. Without a session part, it picks a residential exit that matches your targeting for that connection only. With a session part, it keeps the same exit for that ID for up to the TTL you set.
A sticky IP is a real household connection, so it can disappear before the TTL ends: the device is switched off, loses Wi-Fi or changes network. Long flows must survive that, by checking the exit IP at checkpoints and restarting from a checkpoint instead of failing the whole job.
How does each library reuse connections?
If a library keeps a connection alive, every request on it shares one exit. This is how to get each behaviour on purpose.
| Library | Default | New IP per request | One IP for a flow |
|---|---|---|---|
| Python requests | Session keeps connections alive | Header Connection: close, or a new Session per request | One Session with a session ID in the username |
| httpx | Client pools connections | httpx.Limits(max_keepalive_connections=0) | One client per session ID |
| aiohttp | TCPConnector pools connections | TCPConnector(force_close=True) | Session ID in the proxy URL username |
| Scrapy | Pools per proxy credentials and host | meta={"proxy_session": "new"} with our middleware | A fixed proxy_session for the flow |
| Playwright / Puppeteer | Chrome pools per context | New context per page | One context per session ID |
| Node.js undici, axios, got | Agents keep sockets alive | New agent per request, or keep-alive off | One agent per session ID |
| curl | One connection per command | Already the case | Same session ID in each command |
Which session strategy fits which scraping job?
| Job | Strategy | Suggested TTL |
|---|---|---|
| Independent product, article or listing pages | Rotate: no session ID | — |
| Pagination whose next page depends on a cookie or cursor | One sticky session per listing | 10–30 min |
| Search, then results, then detail pages | One sticky session per search | 10–20 min |
| Headless browser rendering several pages of one site | One sticky session per browser context | The context's life, up to 30 min |
| Checking what a page shows in several cities | One short session per city check | 5 min |
| Re-checking the same page every hour | Rotate | — |
How do I manage a pool of sticky sessions in Python?
A small pool spreads load over a few stable exits and retires each one after a set number of requests or after a sign that its exit is failing, such as 502 or 503. Note what it does not do: it does not swap IPs to get past a 403 or a 429. Those mean the site wants you to slow down, so the pool raises them to your code.
import random
import uuid
import requests
GATEWAY = "gw.crawlrelay.com:7777"
BASE_USER, PASSWORD = "USERNAME-country-us", "PASSWORD"
class SlowDown(Exception):
"""The target asked us to slow down (403 or 429): back off, do not rotate."""
class StickySession:
def __init__(self, ttl=15, max_requests=50):
self.id = uuid.uuid4().hex[:10]
self.max_requests, self.used = max_requests, 0
user = "%s-session-%s-ttl-%d" % (BASE_USER, self.id, ttl)
proxy = "http://%s:%s@%s" % (user, PASSWORD, GATEWAY)
self.http = requests.Session()
self.http.proxies = {"http": proxy, "https": proxy}
class SessionPool:
EXIT_PROBLEMS = {502, 503, 504}
def __init__(self, size=5, **options):
self.options = options
self.sessions = [StickySession(**options) for _ in range(size)]
def _replace(self, i):
self.sessions[i].http.close()
self.sessions[i] = StickySession(**self.options)
def get(self, url, **kwargs):
i = random.randrange(len(self.sessions))
s = self.sessions[i]
s.used += 1
try:
r = s.http.get(url, timeout=(10, 30), **kwargs)
except requests.exceptions.RequestException:
self._replace(i) # dropped exit or timeout: start a fresh session
raise
if r.status_code in (403, 429):
raise SlowDown("%s returned %d" % (url, r.status_code))
if r.status_code in self.EXIT_PROBLEMS or s.used >= s.max_requests:
self._replace(i)
return r
pool = SessionPool(size=5, ttl=15, max_requests=50)
print(pool.get("https://api.ipify.org").text)How do I know that a sticky IP has changed?
Ask. Request an IP echo service such as https://api.ipify.org through the same session at the start of a flow and at each checkpoint, and compare the answers. A change means the exit behind the session changed: restart the flow from its last checkpoint, or open a new session. Log it with the session ID so you can tell IP changes apart from site behaviour when you debug.
Which session mistakes cost the most?
- Rotating on every request of a multi-step flow. The site sees a cart, a cursor or a search token jump between countries and resets it.
- One session ID for every worker. A session is meant for one flow; give each worker and each flow its own ID.
- Sessions far longer than the job. A 30-minute TTL for a 2-minute flow keeps an IP busy for nothing and makes logs harder to read.
- Treating a 429 as an IP problem. A new IP does not change the site's rule; slowing down does. See 429 Too Many Requests.
- Assuming rotation per request. Keep-alive keeps the exit; check the table above for your library.
Sessions and rotation: questions
How long does a sticky session last?
Sticky sessions keep the same IP for the time you set (ttl, in minutes), up to 30 minutes. The IP can change earlier, for example if the residential device goes offline. Choose a TTL slightly longer than the flow it serves, and check the exit at checkpoints.
Does a sticky session cost more than rotation?
No. CrawlRelay bills traffic per GB, and sessions, rotation and targeting are included in that price. A sticky session can even save traffic, because one kept-alive connection avoids a new TLS handshake for every request.
Is rotating on every request better for scraping?
For independent pages, rotating spreads load and avoids hitting one site from one IP for hours. For anything with state, such as pagination tied to a cookie, it breaks the flow. Most crawls mix both: rotate between items, stay sticky within a flow.
Can I choose the exact IP I get?
No. A residential gateway assigns exits from a pool that changes all the time, matching the targeting in your username. You choose the country, city, US state, ZIP code or ASN, and you can keep an IP with a session; you cannot reserve a specific address.
Rotation and sessions are in every plan
$1.50 for 1 GB, $0.50/GB from 50 GB.