Session Stability for Autonomous Agent Infrastructure
Autonomous agents fail differently from scrapers. A scraper that loses its IP mid-crawl retries the URL and moves on. An agent that loses its IP mid-task loses state that exists on the other side of the connection: a login session bound to the old IP, a shopping cart, a paginated search, a CSRF token, a rate-limit allowance it had been carefully spending. The model then observes a confusing failure ("session expired" on step 7 of 12), reasons about it, and frequently makes things worse by re-logging-in or restarting the flow. Token costs balloon and task success rates drop.
This guide is about preventing that failure class with deliberate session design, and about the harder version of the problem: keeping identity coherent when the agent itself is replicated across workers.
Anatomy of a Mid-Task Rotation Failure
A typical sequence when an agent runs on per-request rotation:
- Step 3: agent authenticates to a portal. The server binds the session cookie to IP A.
- Step 5: rotation hands the next request IP B. The server's session validation rejects the cookie.
- Step 6: agent sees a 401, reasons "my login failed", retries authentication from IP B.
- The portal's security layer sees the same account logging in from two IPs in 90 seconds and challenges or locks the account.
- The agent burns its remaining budget reasoning about a problem that was created by infrastructure, not by the task.
Nothing in the agent's prompt can fix this. It must be fixed at the network layer.
The IP Affinity Model
Decide what an "identity" is before assigning sessions. Four scopes cover almost every agent system:
| Affinity scope | One IP per... | Use when |
|---|---|---|
| Task | Unit of work (one checkout, one filing) | Default for tool-use loops |
| Conversation | User-visible chat or job thread | Assistants that interleave many small fetches |
| Account | Credentialed identity on a target site | Anything authenticated — IP changes look like account takeover |
| Replica | Worker process in a fleet | Stateless fan-out workloads with no upstream login |
The most common production mistake is scoping affinity to the replica when the upstream state is bound to an account. If replica 2 picks up a task that replica 1 started against a logged-in portal, replica 2 must present the same session — which means the session key has to be derived from the task or account, never from worker identity.
Deterministic Session Keys
Random session IDs make affinity impossible to recover after a crash. Derive them instead:
import hashlib
import os
GATEWAY = "gate.hexproxies.com:8080"
def session_proxy(scope: str, key: str, generation: int = 0) -> dict:
"""Deterministic sticky-session proxy config.
scope: 'task' | 'conversation' | 'account'
key: stable identifier within that scope (task id, account id)
generation: bump to force a NEW ip for the same logical identity
"""
raw = f"{scope}:{key}:gen{generation}"
sessid = hashlib.sha256(raw.encode()).hexdigest()[:12]
user = os.environ["HEX_PROXY_USER"]
password = os.environ["HEX_PROXY_PASS"]
proxy = f"http://{user}-session-{sessid}:{password}@{GATEWAY}"
return {"http": proxy, "https": proxy}
# Any worker, any restart: same task id -> same session -> same IP.
proxies = session_proxy("task", "order-3417")Because the session ID is a pure function of the task, a crashed worker's replacement reconstructs the identical proxy credentials and resumes with the same egress IP — no shared session-state store required for the happy path.
Failover Without Identity Loss
Sticky sessions can still die: the upstream IP goes unhealthy, or the target starts rejecting it. The critical distinction is between rotating (new IP, keep pretending nothing happened — exactly the failure from the anatomy section) and failing over (new IP, and deliberately resetting upstream state to match):
import httpx
def run_step(scope: str, key: str, url: str, generation: int = 0) -> httpx.Response:
proxies = session_proxy(scope, key, generation)
try:
with httpx.Client(proxy=proxies["https"], timeout=30.0) as client:
response = client.get(url)
if response.status_code in (403, 429):
raise ConnectionError(f"egress IP rejected: {response.status_code}")
return response
except (httpx.ProxyError, httpx.ConnectTimeout, ConnectionError):
if generation >= 2:
raise
# Failover: bump generation -> new deterministic IP,
# then REPLAY the identity-establishing steps (login, consent)
# before resuming the task at the failed step.
reestablish_upstream_state(scope, key, generation + 1)
return run_step(scope, key, url, generation + 1)The generation counter keeps failover deterministic too: every worker that observes generation 1 derives the same replacement IP, so a fleet cannot fan one logical identity out across several IPs during recovery.
Replica-to-Identity Pinning in Distributed Fleets
When tasks fan out across N workers, route by identity, not by worker. A small consistent-hash layer keeps each account's traffic on one session even as the fleet scales:
import { createHash } from 'crypto';
class IdentityRouter {
constructor(private readonly user: string, private readonly pass: string) {}
proxyUrlFor(accountId: string, generation = 0): string {
const sessid = createHash('sha256')
.update('account:' + accountId + ':gen' + generation)
.digest('hex')
.slice(0, 12);
return 'http://' + this.user + '-session-' + sessid + ':' + this.pass
+ '@gate.hexproxies.com:8080';
}
}
// Worker 1 and worker 9 produce byte-identical proxy credentials
// for the same account — affinity survives rescheduling.
const router = new IdentityRouter(process.env.HEX_PROXY_USER!, process.env.HEX_PROXY_PASS!);Header and Fingerprint Coherence
IP affinity is necessary but not sufficient. Real sessions are judged on the bundle: IP, User-Agent, Accept-Language, TLS fingerprint, cookie jar. If your replicas pin the IP but each presents its own runtime's default User-Agent, the bundle is incoherent. Pin the whole bundle to the identity: store the header set and cookie jar keyed by the same scope:key used for the session ID, and load both on every worker that touches the identity. For browser-based agents, persist the browser profile per identity rather than launching a fresh context per step — see the agentic browser automation post for the browser-side half of this problem.
Static ISP vs Sticky Residential
Two ways to get a stable IP, with different trade-offs:
- Dedicated ISP IPs are static by construction — the IP is yours for the life of the plan, registered to Comcast, Frontier, Windstream, or RCN, with unlimited bandwidth at $2.08–$2.47/IP/month. Best for long-lived, high-volume identities (a monitoring agent that polls the same authenticated dashboards daily). Constraint: US-only (Ashburn VA, New York, San Francisco), so they cannot impersonate a non-US user. Details on the ISP proxies page.
- Sticky residential sessions hold one IP per session ID with geo-targeting across 195+ countries (country-level targeting is port-based; US state targeting is ~90% accurate). Bandwidth is metered at $4.25–$4.75/GB. Best for many short-to-medium identities in specific geographies. Honest caveat: a sticky session is held, not owned — design the failover path above before you need it.
For the broader fleet-architecture context (queues, schedulers, worker topology), see multi-agent systems proxy infrastructure; this guide deliberately stays on the session/state layer.
Monitoring Session Health
Track three signals per identity, not per worker: (1) egress IP observed via a periodic echo check — any unexpected change is an affinity bug; (2) upstream auth failures immediately following a proxy error — the signature of rotation-without-replay; (3) generation counter increments — a rising rate means your IPs are being burned and the affinity scope may be too hot. Hex Proxies operates against a 99.9% uptime target; your session layer should still assume individual sessions can fail and make failover boring.