Every scraper that survives past its first week runs into the same realization: blocking isn't really about IPs — it's about sessions. A site tracks you through the combination of IP, cookies, TLS fingerprint, browser state, and behavioral history. Most "we got blocked" stories are actually "our session lifecycle made no sense": one IP that never rotates, cookies that reset on every request, a login session that lived for a month, or identities that change mid-conversation. Anti-bot systems are very good at noticing when these things don't fit the pattern of a real browser user.
Session management for web automation deserves the same design attention you'd give any stateful distributed system. Here are the patterns that work, as a practical catalog.
Pattern 1: The Identity Envelope
Stop thinking "proxy IP" and start thinking identity: the tuple of (exit IP, cookie jar, TLS/browser fingerprint, user agent, locale, timezone). A real user has a consistent envelope — same IP for hours, cookies accumulating history, one browser. A naive scraper has an incoherent one — new IP but old cookies, or old IP and wiped cookies, which no human produces.
The rule: envelopes are born, live, and die together. Rotate the IP → rotate the jar → (with headless browsers) rotate the profile. Never mix generations.
import requests, random, string, hashlib
class IdentityEnvelope:
"""IP + cookie jar + fingerprint headers, born and retired together."""
def __init__(self, geo="us", label=""):
self.sid = label or hashlib.md5(
string.ascii_letters.encode() + str(random.random()).encode()
).hexdigest()[:12]
self.session = requests.Session()
# sticky session => one exit IP for this envelope's whole life
self.session.proxies = {
"http": f"http://thor-user-pass-pw-sessid-{self.sid}-geo-{geo}:pw@proxy.thordata.com:24125",
"https": f"http://thor-user-pass-pw-sessid-{self.sid}-geo-{geo}:pw@proxy.thordata.com:24125",
}
self.ua = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/126.0.0.0 Safari/537.36")
self.session.headers.update({"User-Agent": self.ua,
"Accept-Language": "en-US,en;q=0.9"})
self.born = 0 # requests served
self.max_life = random.randint(80, 300) # envelopes retire, like users leaving
def request(self, method, url, **kw):
self.born += 1
return self.session.request(method, url, timeout=30, **kw)
@property
def expired(self) -> bool:
return self.born >= self.max_life
Note max_life: human sessions end. An identity that requests pages continuously for a week is itself an anomaly. Envelopes should retire at a natural-looking age.
Pattern 2: The Session Pool with Lifecycle States
Treat envelopes like connections in a connection pool, with an explicit state machine: FRESH → ACTIVE → COOLING → RETIRED. Transitions:
- FRESH → ACTIVE: first use; consider a warm-up (see Pattern 4).
- ACTIVE → COOLING: on any soft signal — one 403/challenge, an anomaly in response time, or the age threshold. Cooling is not death: park the envelope for hours or a day; many soft blocks decay.
- ACTIVE/COOLING → RETIRED: hard block (403 with challenge loop, repeated failures), or natural expiry. Retire means discard IP and jar together.
import time
class SessionPool:
def __init__(self, size=10, geo="us"):
self.active, self.cooling, self.retired = [], [], []
self.geo = geo
for _ in range(size):
self.active.append(IdentityEnvelope(geo=self.geo))
def acquire(self) -> IdentityEnvelope:
# recycle cooled envelopes whose cooldown elapsed
revived = [e for e in self.cooling if time.time() - e.cooled_at > 3600]
self.active.extend(revived)
self.cooling = [e for e in self.cooling if e not in revived]
for env in self.active:
if not env.expired:
return env
env = IdentityEnvelope(geo=self.geo) # pool grew organically
self.active.append(env)
return env
def report(self, env: IdentityEnvelope, outcome: str):
if outcome == "ok":
return
self.active.remove(env)
if outcome == "soft": # one challenge — park it
env.cooled_at = time.time()
self.cooling.append(env)
else: # hard block — kill the whole envelope
self.retired.append(env)
The pattern's power: soft blocks become pauses, not losses. Most naive scrapers treat a 403 as "rotate to a new IP immediately," burning through a pool. Cooling preserves the cookie history — often the most expensive thing the envelope owned.
Pattern 3: Session-Affinity by Task
Different tasks need different session stability. Mapping them wrong is a top cause of blocks:
- Login-gated flows → maximum stickiness. One envelope per account, IP never changes for the account's life, cookie jar persisted across runs (a real user logs in from the same machine for months). Changing IP mid-login-session is the classic "account flagged" event.
- Multi-step crawls (search → paginate → detail) → medium stickiness. One envelope per crawl walk; the IP and cookies stay consistent within the walk, and the walk looks like one user's browsing.
- High-volume snapshot collection → low stickiness. Fresh envelope per URL or per few URLs; nothing binds them.
The anti-pattern that gets people blocked: using one long-lived sticky session for high-volume collection "because stickiness is safer." Stickiness concentrates your entire request volume onto one IP — the exact thing rate limits detect. Match stickiness to the task's natural behavioral shape.
Pattern 4: Warm-Up and Human Cadence
Fresh envelopes that immediately hit 50 pages read as bots. Real sessions ramp: land on the homepage, load a couple of assets, navigate, pause. For valuable targets, a 2–3 request warm-up before the real work meaningfully improves survival, especially combined with randomized inter-request delays (2–8s) instead of fixed intervals.
Pattern 5: Persistence and Resurrection
The cookie jar is worth more than the IP. Persist envelopes (jar + metadata + fingerprint config) so that after a worker restart, sessions resume rather than reset. A practical layout: one JSON/pickle file per envelope, loaded lazily, saved after each response — a crash then costs you nothing. For headless-browser automation, persist the full profile directory (localStorage, IndexedDB) the same way. The corollary: never share one jar across two IPs simultaneously. Parallel use of one identity from multiple addresses is unproducible human behavior and one of the strongest correlation signals a defense can see.
Pattern 6: Signals → Lifecycle Feedback
Wire response signals into the state machine, ranked by severity:
- HTTP 200 with challenge markup in body (cf-challenge, captcha iframe) → soft: cool.
- HTTP 403/429 once → soft: cool, and reduce that envelope's future rate.
- Repeated 403 after cooling → retire.
- Account-level responses ("unusual activity") → retire immediately, and never reuse that account/envelope pairing.
- Silent data degradation (prices that stop changing, empty result sets) → treat as suspicion: cool and cross-check with a fresh envelope; degraded-but-200 responses are the sneakiest failure mode in scraping.
Putting It Together
Anti-Patterns to Audit For
Before you refactor anything, audit your current code for the four classic session sins: (1) one requests.Session shared across all workers with a rotating proxy — cookies from identity A arriving at IP B; (2) envelopes that never die — the same sticky session running for weeks; (3) login cookies persisted but the IP randomized every run — an account logging in from a new city each morning; (4) retry storms that hammer a challenged endpoint on the same identity instead of cooling it. All four look harmless in code review and all four are loud signals on the defense side. Fixing them is usually a weekend of work and pays for itself the first week.
The through-line: session management is identity management. The sites you're collecting from don't see your scraper; they see a population of identities. Your job is to make that population demographically boring — each identity coherent from birth to retirement, behaving at human cadence, retiring quietly, never being in two places at once. Get the lifecycle right and you'll find you need far fewer IPs and retries than you thought; the blocks that seemed like "IP quality problems" were usually lifecycle problems wearing a disguise.
Disclosure: I use Thordata's sticky-session residential proxies for the identity envelopes described in this post. If you want to try them, they're at thordata.com, and the code **thor020* gets you 10% off.*
Top comments (0)