Skip to content

Docs: explain how sessions interact with retries (keeping cookies/session across a retry) #2060

Description

@vdusek

Summary

The Session management guide doesn't explain how sessions interact with retries, nor how to keep a session (and its cookies) across a retry. This is a recurring source of confusion.

Motivation

See discussion #2028: a user sets cookies via send_request (or establishes login state), raises to trigger a retry, and finds the cookies "gone" on the retry.

The cookies aren't lost, they're stored on the session. The catch is that an unpinned request draws a new random session on every attempt. When the handler raises and the request is retried, the crawler picks a fresh session from the pool (default max_pool_size=1000), so the retry runs on a different, cookie-less session. Nothing in the docs sets this expectation, and the natural mental model ("same request → same session") is wrong by default.

Proposal

Add a short section to the Session management guide, e.g. "Reusing a session across retries", covering the default behavior and the ways to keep a session across retries.

Option 1 — single-session pool

The simplest fix, keeps a raise-to-retry flow unchanged. One session for everything, so cookies set on the first attempt are still there on the retry. Trade-off: no session rotation.

from crawlee.crawlers import HttpCrawler
from crawlee.sessions import SessionPool


async def main() -> None:
    crawler = HttpCrawler(session_pool=SessionPool(max_pool_size=1))
    # ...

Option 2 — pin the request to its session

Keeps rotation for everything else. Instead of raising, re-enqueue the same URL bound to the session that now has the cookies. The user_data flag guards against re-enqueuing in a loop, and always_enqueue=True bypasses deduplication so the same URL runs again.

from crawlee import Request
from crawlee.crawlers import HttpCrawler, HttpCrawlingContext

crawler = HttpCrawler()


@crawler.router.default_handler
async def handler(context: HttpCrawlingContext) -> None:
    if context.request.user_data.get('prepared'):
        # Cookies from the setup step are on this pinned session.
        # ... do the real work ...
        return

    # First pass: establish cookies on the current session.
    await context.send_request('https://example.com/set-something')

    # Re-enqueue the same URL, pinned to the same session.
    await context.add_requests(
        [
            Request.from_url(
                context.request.url,
                session_id=context.session.id,  # Pins the retry to this session.
                user_data={'prepared': True},
                always_enqueue=True,  # Bypasses deduplication so the same URL runs again.
            )
        ]
    )

Option 3 — re-apply cookies in a pre-navigation hook

Keeps the raise-to-retry flow and rotation, at the cost of some manual plumbing. Snapshot the cookies after the setup step into an external store keyed by the request's unique_key, then re-apply them onto whichever session the retry picks in a pre_navigation_hook (which runs before navigation and receives context.session).

from crawlee.crawlers import HttpCrawler, HttpCrawlingContext

crawler = HttpCrawler()
pending_cookies: dict[str, list] = {}


@crawler.router.default_handler
async def handler(context: HttpCrawlingContext) -> None:
    if not has_what_i_need(context):  # your existing check, e.g. cookies present
        # First pass: establish cookies, then snapshot them for the retry.
        await context.send_request('https://example.com/set-something')
        pending_cookies[context.request.unique_key] = context.session.cookies.get_cookies_as_dicts()
        raise RuntimeError('retry with cookies')

    pending_cookies.pop(context.request.unique_key, None)
    # ... do the real work; cookies are on context.session ...


@crawler.pre_navigation_hook
async def restore_cookies(context: HttpCrawlingContext) -> None:
    if saved := pending_cookies.get(context.request.unique_key):
        context.session.cookies.set_cookies(saved)

Note: use an external store (as above), not context.request.user_data, to carry the snapshot across the retry. During a handler run context.request is an isolated copy, so in-handler mutations to it (including user_data) don't survive the retry (see #2061).

The guide should also note that cookies are scoped by domain (the send_request target must set them for the same host you're crawling), and that a custom HTTP client needs to attach the session's cookies to outgoing send_request calls.

Notes for a future redesign (v2)

Reusing a session across retries isn't ergonomic today, and we expect to rethink this in Crawlee for Python v2 / Crawlee for JS v4. Worth capturing here as prior art / parity references:

  • Crawlee for JS Session management and its SessionPool / sessionPoolOptions.maxPoolSize.
  • Related parity gap: in Crawlee for JS, request.userData mutations persist across retries (same Request instance), whereas in Crawlee for Python context.request is an isolated deepcopy during handler execution, so in-handler mutations to context.request (including user_data) are silently dropped on retry. That difference makes some of the natural workarounds (stash state on request.user_data, then read it on the retry) fail. This is worth deciding on explicitly as part of the redesign.

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentation.t-toolingIssues with this label are in the ownership of the tooling team.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions