The Complete Guide to Playwright storage_state — Saving, Reusing, and Managing Multiple Roles

If your Playwright tests feel slow because every one of them logs in from scratch, storage_state is the answer. By saving cookies and localStorage after login to a file and loading it in later tests, you can skip the login flow entirely. This article covers everything from the basics of saving and loading, to wiring it into pytest-playwright, managing multiple roles, and the practical pitfalls around expiry, parallel execution, and sensitive data.

storage_state isn’t a “login shortcut” — it’s a way of managing authentication state as a file. Understanding what gets saved, and what doesn’t, cuts down on mysterious authentication errors.

📌 Who This Article Is For

  • Playwright (Python) users who log in at the start of every test and are watching their runtime grow
  • Anyone who knows the name storage_state but is fuzzy on saving it to a file and the rules for using it
  • Anyone who wants to write tests for multiple roles, like admin and regular user
  • Anyone whose authentication gets flaky in CI or under parallel execution

✅ What You’ll Learn

  • How to save authentication state to a file and apply it to pytest-playwright’s default page
  • What gets saved and what doesn’t (sessionStorage, for example)
  • How to handle multiple roles, expiry, parallel execution, and sensitive data

👤 About the Author

Written by Yoshi, a QA and test automation engineer with over 15 years of hands-on experience, continuously refining how authenticated test infrastructure is designed and run with Playwright and pytest.

📌 Key Takeaways

  • Save with context.storage_state(path=...) and load with storage_state= to skip the login flow
  • Cookies and localStorage are saved by default, and IndexedDB can be saved too when needed (indexed_db=True). sessionStorage is not included
  • The saved file contains session data, so keeping it out of Git and being mindful of expiry are essential

When testing apps that require login, these problems tend to come up:

  • Every test drives the login screen, adding a few seconds apiece
  • A change to the login screen takes down unrelated tests all at once
  • Tests for different roles, like admin and regular user, aren’t well organized
  • Things got unstable once you turned on parallel execution

Designing your storage_state usage properly goes a long way toward easing all of them.

Where This Article Fits

Our advanced Page Object Model article introduces the idea of “create authentication state once in a session-scoped fixture and reuse it” as one part of POM design. This article pulls that piece out and goes deeper, focusing on the operational side: saving to a file, multiple roles, expiry, CI, and parallel execution. If you want to see how it fits together with POM, read the advanced article as well.

💡 Related: Advanced Page Object Model in Playwright — Pairing pytest Fixtures with Component Design for Better Maintainability covers how session-scoped authentication fits into a POM setup.

What Is storage_state, and What Gets Saved?

storage_state is a feature for saving the state of a browser context — cookies, localStorage, and so on — and carrying it over to another context. Reusing a logged-in state is its most common use.

ItemSavedNotes
Cookies✅Session cookies and the like
localStorage✅Used by SPAs that store things like a JWT
sessionStorage✕Not saved by storage_state. Apps that keep auth data here need a different approach
IndexedDB✅※※ When you pass indexed_db=True (added in v1.51 — confirm against the docs for your version)

① Saving Authentication State to a File

Here’s the basic form: log in, then write storage_state out to a file.

from playwright.sync_api import Browser

def save_auth_state(browser: Browser, base_url: str, path: str) -> None:
    context = browser.new_context()
    page = context.new_page()

    page.goto(f"{base_url}/login")
    page.get_by_label("Email").fill("test@example.com")
    page.get_by_label("Password").fill("Password123")
    page.get_by_role("button", name="Log in").click()
    page.wait_for_url("**/dashboard")  # confirm login finished before saving

    context.storage_state(path=path)  # write out as a JSON file
    context.close()

If your app keeps authentication data in IndexedDB (Firebase Authentication, for example), pass indexed_db=True when saving.

# Include IndexedDB in the saved state (v1.51 and later)
context.storage_state(path=path, indexed_db=True)
⚠️ Note: Saving storage_state before login finishes leaves you with a file that’s nearly empty of authentication data. Confirm the post-login state with wait_for_url() or expect() before saving.

② Applying It to pytest-playwright’s Default page

With pytest-playwright, overriding the browser_context_args fixture applies the saved state to any test that uses the default context and page fixtures.

# conftest.py
import os
import pytest

AUTH_FILE = "auth/user.json"

@pytest.fixture(scope="session", autouse=True)
def prepare_auth(browser, base_url):
    """Log in once before the tests start, and save the auth state."""
    os.makedirs("auth", exist_ok=True)
    save_auth_state(browser, base_url, AUTH_FILE)

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    """Apply the saved auth state to the default context."""
    return {**browser_context_args, "storage_state": AUTH_FILE}
# The test starts already logged in
def test_dashboard(page):
    page.goto("/dashboard")
    assert page.get_by_text("Welcome").is_visible()
🔍 From the Field: Applying it through browser_context_args means even tests of the login screen itself, if they use the default page, start out logged in. For tests that need to see the pre-login state, create a separate context with browser.new_context(), or provide a dedicated unauthenticated fixture. Also note that if you call browser.new_context() yourself, the browser_context_args settings are not applied automatically — pass storage_state= explicitly, as in ③.

③ Switching Between Multiple Roles

If you keep a separate file per role — admin and regular user, say — each test can pick the role it needs.

# conftest.py
import pytest

ROLE_FILES = {
    "admin": "auth/admin.json",
    "user": "auth/user.json",
}

@pytest.fixture
def page_as(browser):
    """A factory fixture that returns a page already logged in as the given role."""
    contexts = []

    def _page_as(role: str):
        context = browser.new_context(storage_state=ROLE_FILES[role])
        contexts.append(context)
        return context.new_page()

    yield _page_as

    for context in contexts:
        context.close()  # don't forget the cleanup
def test_admin_can_see_user_list(page_as):
    page = page_as("admin")
    page.goto("/admin/users")
    assert page.get_by_role("heading", name="User Management").is_visible()

def test_user_cannot_access_admin_page(page_as):
    page = page_as("user")
    page.goto("/admin/users")
    assert page.get_by_text("You do not have permission").is_visible()
💡 Practical Tip: When a single test needs several roles — “act as admin, then verify on a regular user’s screen” — just call page_as twice to get two independent contexts.

④ Planning for Expiry

A saved file doesn’t last forever. Once the session expires, tests start failing with “mysterious authentication errors.”

import os
import time

MAX_AGE_SECONDS = 30 * 60  # example: only reuse files modified within 30 minutes

def is_auth_file_fresh(path: str) -> bool:
    """True if the file exists and was modified recently enough."""
    if not os.path.exists(path):
        return False
    age = time.time() - os.path.getmtime(path)
    return age < MAX_AGE_SECONDS

@pytest.fixture(scope="session", autouse=True)
def prepare_auth(browser, base_url):
    if not is_auth_file_fresh(AUTH_FILE):
        os.makedirs("auth", exist_ok=True)
        save_auth_state(browser, base_url, AUTH_FILE)
⚠️ Note: The file’s modification time is only a rule of thumb for regenerating files that have gotten too old. It does not directly check a cookie’s expires value or the server-side session lifetime. Find out the real expiry, and set MAX_AGE_SECONDS shorter than that to be safe.

⑤ CI, Parallel Execution, and Sensitive Data

ConcernWhat to do
Version controlThe auth file contains session data, so add auth/ to .gitignore and never commit it. The official Playwright docs also use a playwright/.auth/ layout
CIGenerate it by logging in within the job. Keep credentials in Secrets, and don’t publish the auth file as an artifact
Parallel execution (pytest-xdist)With pytest-xdist, session fixtures run once per worker, so a setup where every worker generates the same auth file at once risks write conflicts. Give each worker its own file name, or generate it once up front
Sharing one accountIf one test logs out or changes the password, the server-side session can be invalidated, causing other tests using the same state to fail

To split files per worker, use the worker_id fixture provided by pytest-xdist. The official Playwright authentication guide also describes splitting auth state files per worker for parallel runs (using parallelIndex on the Node side). Don’t just rename the file — make both the save step and the load step use the same auth_file.

# conftest.py (the fixed-AUTH_FILE version from ②, rewritten for parallel runs)
@pytest.fixture(scope="session")
def auth_file(worker_id):
    # without xdist: user_master.json / with it: user_gw0.json, user_gw1.json ...
    os.makedirs("auth", exist_ok=True)
    return f"auth/user_{worker_id}.json"

@pytest.fixture(scope="session", autouse=True)
def prepare_auth(browser, base_url, auth_file):
    save_auth_state(browser, base_url, auth_file)  # the save path is now per-worker

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args, auth_file):
    return {**browser_context_args, "storage_state": auth_file}  # same file when loading
🔍 From the Field: “Only the logout test takes down everyone else” is a classic pattern. Tests that break authentication state — logout, password change — are safer run with a dedicated account and their own individual login, rather than the shared file.

Common Mistakes Seen in Practice

① Committing the auth file to Git

auth.json contains session cookies and tokens. Putting it in the repository is a leak risk, so add it to .gitignore from the very beginning.

② Saving before login completes, then reusing an empty state

If you save too early, a file with no authentication data gets applied to every test, and all of them fail with auth errors. Always confirm the post-login state before saving.

③ Can’t reproduce login for an app that keeps auth in sessionStorage

Since sessionStorage isn’t saved, storage_state alone can’t restore the logged-in state for apps that keep authentication data there. You’ll need another approach (such as injecting the values with an init script), so check how your app handles authentication first.

④ Keeping an old file and missing that it expired

Reusing a stale auth.json left on your machine means tests suddenly start failing one day. Either build in a freshness check, or regenerate the file on every test run.

FAQ

Q. How many times can I reuse a saved auth file?

It’s governed by the app’s session lifetime, not a count. You can reuse it as many times as you like until the session expires. For apps with short sessions, regenerating it on every run is the safer approach.

Q. Can I log in through an API instead of the UI and save the state?

Yes. You obtain a token or cookie via the API, set it on a browser context, and then save storage_state. It’s faster since it skips UI interaction, but the steps depend on your app’s authentication method, so check the details for your case.

Q. Does storage_state work with APIRequestContext too?

Yes. Passing it like playwright.request.new_context(storage_state=...) sends API requests with the saved cookies attached.

Q. Is it okay to open and look at the auth file?

Looking is fine, but be careful not to include its contents in screen shares, logs, or screenshots. Cookie values and tokens are stored as-is, so treat it with the same care as a password.

Q. Does this make tests less reliable than logging in every time?

Not significantly, as long as you keep separate tests for the login flow itself. The basic design is to separate “tests of login” from “tests of features that assume you’re already logged in.”

📋 Summary

  • storage_state saves and reuses cookies and localStorage, and can include IndexedDB when needed. sessionStorage is not included
  • With pytest-playwright, override browser_context_args to apply it to tests that use the default page
  • Keep a separate file per role and switch between them with a factory fixture
  • Three things hold the operation together: keep it out of Git, mind expiry, and watch out for parallel runs and shared accounts

Repeating the login flow quietly pushes up your suite’s runtime. Start by trying save-and-load with a single role, and once that works, expand to more roles and the operational rules above.

Copied title and URL