Your agent is twelve steps into a fourteen-step checkout flow—cart built, shipping address entered, promo code applied—when the worker process gets OOM-killed. Restart it, and it starts from step one. Same cart, same address, same promo code, re-typed by an LLM that has no idea it already did this. Steel.dev called their version of this problem out in June with a "durable researcher" that checkpoints every step; Browserbase built Autobrowse around a similar instinct. The pattern is the same everywhere: the browser remembers nothing about the process that was driving it, and the process remembers nothing once it restarts.
The fix isn't a bigger retry budget. It's separating three things that most agent scripts collapse into one: the browser session, the task progress, and the client process. Get those three out of lockstep and a crash costs you seconds, not a full re-run.
The browser session outlives your process
An Anchor session runs server-side. Your Python process connecting to it over CDP is just a client—kill that process and the browser, the DOM, the cookies, and the login state are all still sitting there, untouched:
from anchor_browser import AnchorClient
anchor = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])
session = anchor.sessions.create(
session={
"timeout": {
"max_duration": 30, # hard cap, minutes
"idle_timeout": 10, # survives a crash + restart window
}
},
proxy_country="us",
)
checkpoint_store.set(task_id, {"session_id": session.data.id, "step": 0})
The only thing worth persisting outside the process is session.data.id. Store it somewhere that survives a restart—Redis, a row in Postgres, even a local file for a single-worker setup.
Checkpoint the task, not just the browser
A live session tells you nothing about which of your fourteen steps already ran. That's a separate, much smaller piece of state:
def save_checkpoint(task_id: str, session_id: str, step: int, data: dict):
checkpoint_store.set(task_id, {
"session_id": session_id,
"step": step,
"data": data,
})
def load_checkpoint(task_id: str) -> dict | None:
return checkpoint_store.get(task_id)
Write a checkpoint after every step completes, not before—you want to resume after the last confirmed action, never mid-action.
Resuming after the crash
On startup, check for an existing checkpoint before creating anything new. If one exists, reconnect to the same live session and skip straight to the step you left off on:
async def run_checkout(task_id: str, steps: list):
checkpoint = load_checkpoint(task_id)
if checkpoint:
session_id = checkpoint["session_id"]
start_step = checkpoint["step"]
session = anchor.sessions.get(session_id) # still alive, still logged in
else:
session = anchor.sessions.create(proxy_country="us")
start_step = 0
save_checkpoint(task_id, session.data.id, 0, {})
browser = await playwright_chromium.connect_over_cdp(session.data.cdp_url)
page = browser.contexts[0].pages[0]
for i in range(start_step, len(steps)):
await steps[i](page)
save_checkpoint(task_id, session.data.id, i + 1, {})
return "completed"
No re-login, no rebuilt cart, no re-applied promo code. The agent resumes exactly where the process died, because the thing that actually held the state—the browser—was never the thing that crashed.
Production notes
Set idle_timeout wider than your expected restart window. A session that dies before your orchestrator restarts defeats the whole pattern—size it to your deploy or crash-loop recovery time, not just "a few minutes."
Guard side-effecting steps with idempotency keys. "Click submit" is safe to skip on resume; "charge the card" is not. Checkpoint immediately after any step that isn't safe to repeat, and never before it.
Expire checkpoints you don't resume. A dead session with a lingering checkpoint entry will fail sessions.get—catch that, discard the checkpoint, and fall back to a clean run rather than retrying forever against a session that no longer exists.
Get started
Anchor session timeout documentation · Anchor documentation · Start your free Anchor trial →



