Root Cause Diagnosis
Chromium’s cookie store only writes cookies with an explicit Expires or Max-Age attribute to the on-disk Cookies SQLite database inside the user_data_dir. Cookies issued without either attribute are, per RFC 6265, session cookies — Chromium keeps them in memory only and deliberately purges them on a clean shutdown, regardless of whether you launched via launch_persistent_context. A persistent user_data_dir guarantees profile and cookie-jar persistence, but it does not override Chromium’s own session-cookie lifecycle rules. This is compounded when context.close() is never reached (script crash, unhandled exception, SIGKILL, CI timeout kill) — Chromium’s SQLite Cookies DB uses a WAL file that only gets checkpointed to the main DB on graceful shutdown, so even legitimately persistent cookies can be lost on an unclean exit.
Quick Fix
Stop relying on the profile directory to carry session cookies across runs. Serialize the full cookie jar with storage_state, which captures session cookies that Chromium’s native persistence discards:
context.storage_state(path="state.json")
On the next run, load it explicitly instead of depending solely on user_data_dir:
context = browser_type.launch_persistent_context(
user_data_dir="./profile",
storage_state="state.json"
)
Step-by-Step Resolution
1. Guarantee close() always runs
Wrap context usage so a crash or exception mid-script doesn’t skip the flush:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
context = p.chromium.launch_persistent_context(
user_data_dir="./profile",
headless=False,
)
try:
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://example.com")
# ... automation steps ...
finally:
context.storage_state(path="state.json")
context.close()
context.close() inside finally ensures the WAL checkpoint fires even if an assertion or navigation error interrupts the script.
2. Confirm which cookies are actually being dropped
Before closing, inspect Expires on the cookies you care about:
for cookie in context.cookies():
print(cookie["name"], cookie.get("expires"))
An expires value of -1 means the cookie is session-scoped and was never going to survive a native browser close — this is expected Chromium behavior, not a Playwright bug.
3. Force session cookies to persist, if the target site allows it
If you control the cookie’s origin (a login endpoint you’re testing against, or you’re rewriting it via add_cookies), set an explicit expiry so Chromium’s native store treats it as persistent:
context.add_cookies([{
"name": "session_id",
"value": "abc123",
"domain": "example.com",
"path": "/",
"expires": 1893456000 # explicit Unix timestamp, not -1
}])
4. For third-party session cookies you don’t control, rely on storage_state, not the profile dir
# save immediately after authentication succeeds
context.storage_state(path="auth_state.json")
# next run: rehydrate explicitly instead of trusting user_data_dir alone
context = browser_type.launch_persistent_context(
user_data_dir="./profile",
storage_state="auth_state.json"
)
storage_state serializes the entire cookie set Playwright’s context is tracking at the moment you call it — including session cookies — independent of Chromium’s own persistence rules.
5. Check the profile’s exit_type after abrupt termination
If a process was killed rather than closed cleanly, inspect:
cat ./profile/Default/Preferences | grep -o '"exit_type":"[^"]*"'
"exit_type":"Crashed" confirms Chromium flagged the last session as unclean, which is consistent with WAL-checkpoint loss on the Cookies DB. Setting it to "Normal" will not recover lost data but confirms the diagnosis for future runs.
Why This Fails for Some Users
Headless mode masking the real cause. In headless Chromium, some session-restore prompts and crash-recovery UI never render, so a script that force-kills the browser process (os.kill, container OOM, CI job timeout) looks identical to a clean run in logs — the cookie loss gets misattributed to “Playwright dropping cookies” rather than an unclean process exit.
Incognito/ephemeral context confusion. browser.new_context() (non-persistent) never writes to disk at all — cookies live only in memory for that context’s lifetime. Users sometimes mix this with launch_persistent_context across different parts of the same codebase and see inconsistent persistence depending on which API path created the context.
Docker and read-only filesystem mounts. When user_data_dir points into a container volume mounted with restricted write permissions, or an ephemeral /tmp that gets wiped between container restarts, the Cookies SQLite file and its WAL can silently fail to persist between runs even though no error is raised at the Python level — sqlite3 write failures inside Chromium’s internals aren’t surfaced through the Playwright API.
Multiple concurrent contexts sharing one user_data_dir. Launching two launch_persistent_context processes against the same directory causes SQLite lock contention on the Cookies DB; the second process can silently lose writes rather than raising a Python-level exception, since the failure happens inside the Chromium process, not the driver.