If you have ever watched a CI job spend the first eight seconds of every single test typing a username and password into a login form, you already know the tax. Re-login every test automation is the default behavior of most browser test tools: fresh browser, fresh context, fresh cookie jar, every time. It feels safe. It is also slow, brittle, and in practice the thing most likely to make your suite flaky before your actual feature code ever gets exercised.
This article is about naming that cost honestly, showing where it bites hardest, and walking through how browserbash auth save and --auth remove it without asking you to build a custom fixture layer or hand-roll cookie injection.
Why Fresh Context Per Test Became the Default
Nobody chose "log in every test" because it was fast. It became the default because it is the safe, obviously-correct thing to do when you don't trust state to carry over cleanly between tests. A fresh browser context has no leftover cookies, no stale local storage, no half-finished checkout in a cart. Test isolation is a real and valuable property, and for a long time the easiest way to get it was to throw away everything and start over, including the authenticated session.
The problem is that "start over" got bundled with "re-authenticate" as if they were the same requirement. They are not. You can have a perfectly clean, isolated browser context that still starts already logged in, the same way a human tester doesn't close their laptop and re-type their password before checking the next ticket. Most tooling just never gave you an easy way to separate those two concerns, so re-login every test automation became the path of least resistance, and it calcified into "how testing works" long after the actual justification stopped applying.
Where the Re-Login Tax Actually Shows Up
The cost is easiest to see once you break it into pieces, because it is rarely one big obvious problem. It is five small ones that compound.
Wall-clock time. A login flow that takes 4 to 10 seconds by hand (navigate, fill email, fill password, click submit, wait for redirect, wait for the dashboard to hydrate) gets multiplied by every test in the suite. A 50-test suite with an 8-second login step burns nearly 7 minutes on nothing but authentication before a single assertion runs.
Model or agent cost. If you are driving tests with an AI agent, whether that is BrowserBash, Stagehand, or a hand-rolled LLM loop, the login form is not free reasoning. The agent has to look at the page, find the email field, find the password field, decide when the redirect finished, and confirm it landed somewhere authenticated. That is real tokens and real latency spent on a task that produces zero test signal.
Rate limits and lockouts. Auth endpoints are frequently the most aggressively rate-limited route in an app, on purpose, because they are also the most attacked route in production. Fire fifty test runs at a login endpoint in ten minutes and you can trip the same throttling meant to stop credential-stuffing bots. Now your CI is red not because of a bug, but because your own test infrastructure looks like an attack.
MFA and captcha walls. Any serious login flow eventually adds a second factor, an email verification link, or a captcha challenge, usually after it detects repeated automated-looking logins from the same IP or same test account. A single login in a person's normal day rarely trips this. Fifty scripted logins in the same CI run absolutely will, and now your "flaky test" is actually a security control doing exactly what it was built to do.
Flake that has nothing to do with your feature. When the login step is part of every test, a login flow regression, a slow auth provider, a network blip, or an SSO redirect hiccup shows up as a failure in every downstream test, not just the login test. Triage gets harder because the failure signature (a timeout on the dashboard heading) looks identical whether the actual bug is in checkout or in the OAuth callback three steps earlier.
None of these individually sounds catastrophic. Together, across a suite that runs on every PR, they add up to slower feedback loops, noisier CI, and engineers who start ignoring red builds because "it's probably just the login flake again."
Why "Just Log In Faster" Doesn't Fix It
The instinctive first fix is to optimize the login flow itself: cache a JWT, hit an API login endpoint directly instead of the UI, inject a cookie before navigation. All of these work, and all of them require you to maintain a second, parallel path into your app that has nothing to do with what a real user does. That path drifts. Your auth provider adds a new claim, your session cookie gets a new flag, your API contract changes in a way the UI login never notices, and your "fast login" helper silently starts producing sessions the app doesn't actually trust anymore. You find out when a test that was "logged in" hits a page that redirects it straight back to the login screen, and now you are debugging your test infrastructure instead of your product.
The other instinctive fix is to just accept the tax and throw hardware at it: more parallel workers, more CI minutes. That doesn't reduce the rate-limit and captcha risk at all, it makes it worse, because now you have more concurrent logins hitting the same auth endpoint from the same CI IP range at the same moment.
What actually removes the tax without introducing a parallel fake-auth path is capturing a real, browser-produced session once, and reusing it. That's the entire idea behind saved sessions.
The Fix: Saved Sessions with browserbash auth save
BrowserBash ships a saved-logins feature built for exactly this. You run it once, interactively:
browserbash auth save staging-admin --url https://app.example.com/login
This opens a real browser window pointed at your login URL. You log in the normal way, by hand, including whatever MFA, SSO redirect, or captcha your app actually requires in production. Once you're through and the app is showing you an authenticated page, you press Enter in the terminal. BrowserBash captures the browser's storage state (cookies, local storage, session storage, the works) exactly as the real login flow produced it, and saves it as a named profile under staging-admin.
From that point on, any run can start already authenticated:
browserbash run "Open the billing page and verify the current plan is Pro" --auth staging-admin
No login step. No typed email, no typed password, no waiting for a redirect. The browser context boots with the same session a real login would have produced, and the objective starts on whatever page you actually care about testing.
The same flag works across the surfaces you'd expect:
# a single markdown test
browserbash testmd run ./.browserbash/tests/checkout_test.md --auth staging-admin
# a whole suite, sharded across CI machines
browserbash run-all .browserbash/tests --auth staging-admin --shard 2/4 --budget-usd 2
# an always-on monitor that alerts only on state changes
browserbash monitor "Confirm the dashboard loads and shows recent orders" \
--auth staging-admin --every 10m --notify https://hooks.slack.com/services/XXX
You can also set it once in a test file's frontmatter instead of repeating the flag on every invocation, which matters once you have more than a handful of tests sharing a login profile:
---
auth: staging-admin
---
# Verify billing page shows correct plan
- Open the billing page
- Verify 'Pro Plan' text visible
If the saved profile's origins don't cover wherever the test tries to start, BrowserBash prints a warning instead of silently pretending the session applies. That matters more than it sounds like: a silent no-op would make you think auth reuse was broken when actually the profile was just saved against the wrong environment (say, production instead of staging), and you'd waste time debugging the wrong layer.
Under the Hood: Why This Is Safe Isolation, Not a Shortcut
It's worth being precise about what "saved session" actually means here, because the whole point is that it shouldn't compromise test isolation. BrowserBash uses Playwright's storageState mechanism: a JSON snapshot of cookies and web storage captured from a real, browser-driven login. Every run that uses --auth still gets its own fresh browser context. Nothing carries over between test runs except the one thing you explicitly asked to carry over, the authenticated session itself. Your test isn't sharing a live browser tab with the previous test, it isn't inheriting a half-submitted form, it isn't picking up leftover local storage from an unrelated feature. It's starting clean, just clean-and-logged-in instead of clean-and-logged-out.
This is also why saved sessions don't create the parallel-path drift problem that hand-rolled cookie injection does. The session in the profile was produced by the actual login UI, going through the actual auth provider, actual MFA, actual redirects. If your auth provider changes something structural, the fix is the same one-time interactive step: run auth save again to refresh the profile. You are not maintaining a second implementation of login logic that can silently diverge from the real one.
Before and After: What This Looks Like on a Real Suite
To make the cost concrete, picture a fairly ordinary regression suite: 40 tests, each of which needs an authenticated session before it can do anything useful, running with a login step that takes roughly 7 seconds end to end (navigate, fill, submit, wait for redirect, confirm landing page).
| Login every test | Saved session (--auth) |
|
|---|---|---|
| Per-test login overhead | ~7s | 0s (session pre-loaded) |
| Suite-wide login overhead (40 tests) | ~4.7 minutes | ~0 minutes |
| Auth endpoint hits per suite run | 40 | 1 (during auth save, done once, reused indefinitely) |
| Rate-limit / lockout exposure | High under parallel CI workers | Effectively eliminated |
| MFA/captcha risk from repeated automated logins | Present, grows with suite size | Removed (login only happens interactively, by a human, once) |
| What breaks if the login UI changes | Every test in the suite, simultaneously | Nothing, until you choose to refresh the saved profile |
This table is illustrative, not a benchmark claim, your own numbers depend on how slow your specific login flow is and how many tests share it. But the shape of the trade is consistent across almost every suite we've seen: the login step is rarely the interesting part of the test, and paying its cost forty times instead of once is pure overhead. On CI with run-all --shard, the effect compounds further, because each shard would otherwise be independently hammering the same auth endpoint in parallel, which is exactly the pattern that trips rate limiters fastest.
Rolling This Out Across a Real Suite
The practical rollout looks like this. First, identify how many distinct "identities" your suite actually needs, not how many tests. Most suites need far fewer auth profiles than they think: an admin account, a regular user account, maybe a read-only or trial-tier account for permission-boundary tests. Save each one:
browserbash auth save admin-user --url https://staging.example.com/login
browserbash auth save trial-user --url https://staging.example.com/login
Second, add auth: frontmatter to the test files that need each identity, or standardize on --auth in your CI invocation if every test in a directory uses the same one. Third, and this is easy to skip, decide who owns refreshing profiles and when. Saved sessions are not eternal. Real sessions expire, get revoked by security policy, or stop working after a password rotation. Treat auth save refresh as a small recurring maintenance task, not a one-time setup step you forget about until a suite mysteriously starts failing every test at once (which, notably, is a much easier failure to diagnose than "every test is independently a little bit flaky at the login step," because it fails clearly and immediately instead of intermittently).
If you're running in CI, save the profile once against a stable staging environment and commit the resulting reuse pattern (not the storage state file itself, treat that as a secret, store it the way you'd store any credential artifact) into your pipeline setup so every shard and every parallel worker reuses the same identity instead of each independently logging in.
When Fresh Login Per Test Still Makes Sense
Saved sessions are not the right call for every test, and it's worth being honest about where re-authenticating on purpose is still correct.
Testing the login flow itself. If the point of the test is to verify that login works, that MFA prompts correctly, that a wrong password shows the right error message, you obviously need to exercise the real flow, not skip it. Keep a small, dedicated set of tests that do full fresh authentication and treat those as your canary for auth regressions.
Session-boundary and expiry behavior. Tests that specifically check what happens when a session expires mid-flow, or that a logged-out user gets redirected correctly, need to start from a genuinely unauthenticated state on purpose.
Cross-account isolation checks. If you're testing that user A truly cannot see user B's data, you want two distinct fresh sessions in the same test run, not a shared cached one, to make sure you're not accidentally testing against browser-level caching artifacts instead of actual server-side authorization.
One-time or rarely-run exploratory checks. If a test runs once a quarter, the login overhead was never the bottleneck, and there's little value in maintaining a saved profile for it.
The rule of thumb: save sessions for the tests where authentication is a precondition, not the subject under test. That's the overwhelming majority of a typical regression suite, checkout flows, dashboard checks, settings pages, permission-gated features, but it deliberately excludes the handful of tests whose entire job is to validate the login experience itself.
How This Fits the Rest of a BrowserBash Suite
Saved sessions compose with everything else in the CLI rather than replacing it. A --auth profile works alongside the replay cache, so a suite that's already skipping model calls on unchanged pages now also skips the login step, compounding the token and time savings rather than just adding them. It works inside run-all --shard 2/4 --budget-usd 2 for CI fleets that split a suite across machines and want a hard cost ceiling. It works with browserbash monitor --every 10m --notify, which matters a lot for synthetic monitoring specifically, because a monitor that logs in fresh every ten minutes for months on end is exactly the pattern most likely to eventually trip a captcha wall or get its IP flagged, while a monitor running against a saved, periodically-refreshed session looks like ordinary return traffic.
It's also worth connecting this to the MCP server, since AI coding agents using BrowserBash as a validation layer through run_objective or run_test_file benefit the same way a human-triggered CI run does: an agent iterating on a checkout bug fix and re-verifying twenty times in an afternoon doesn't need to re-solve the login form twenty times, it needs --auth doing the boring part so the agent's reasoning budget goes toward the actual bug.
If your test files use testmd v2 with API steps for seeding data, the pattern combines cleanly too: seed state through an API call, then verify it through an already-authenticated UI session, without paying for either a UI login or a second, redundant token-fetch step.
---
version: 2
auth: staging-admin
---
# Verify a seeded order appears in the dashboard
1. POST https://api.example.com/orders with body {"sku": "WIDGET-1", "qty": 2}
2. Expect status 201, store $.id as 'orderId'
3. Open the orders dashboard
4. Verify '{{orderId}}' text visible
That test never touches the login form at all. It seeds through the API deterministically, then checks the UI through a session that was captured once, by hand, and reused. No agent reasoning spent on either the login screen or on judging whether the order "looks" present, the Verify step compiles to a real, deterministic Playwright check against {{orderId}}.
The Broader Point: Isolation and Re-Authentication Are Different Requirements
The re-login-every-test tax survives mostly because it's the default nobody questioned, not because it's the correct trade-off for most suites. Test isolation, a clean context with no leftover state from the previous test, is genuinely valuable and worth keeping. Re-authenticating from scratch on every single run is a separate decision that happens to have been bundled with isolation for historical reasons, and unbundling it is almost always a net win once you separate "does this test need a clean slate" from "does this test need to prove the login form works." Most tests need the former and not the latter.
Once you save a session for the identities your suite actually uses and point --auth at them, the login step stops being something your suite pays for forty separate times per run. It becomes a five-minute task you do by hand, occasionally, the same way you'd log into your email once in the morning instead of re-authenticating before reading every single message.
FAQ
Does browserbash auth save store my password?
No. The interactive auth save step opens a real browser, you type your credentials directly into the actual login form the same way you always would, and BrowserBash never sees or stores the password itself. What gets saved is the resulting browser storage state (cookies and local/session storage) produced after that login completes, which is the same mechanism Playwright's storageState uses.
How is this different from just hardcoding a cookie or a JWT in my test setup?
Hardcoding a token means maintaining a second, parallel path into your app's auth that can silently drift from what the real login UI produces, for example if your provider adds a new required cookie flag or claim. A saved session is captured directly from a real, browser-driven login through your actual auth provider, MFA, and redirects, so it stays representative of what a real user session actually looks like.
Will reusing a saved session break test isolation between runs?
No. Every run still gets a fresh browser context; the only thing carried over is the specific authenticated session you attached with --auth. There's no shared browser tab, no leftover form state, and no cross-test contamination beyond the identity you deliberately chose to reuse.
Do I still need tests that log in fresh?
Yes, keep a small set of dedicated tests that exercise the real login flow, including MFA and error states, since those are testing the login experience itself. Saved sessions are for the much larger set of tests where authentication is a precondition to reach the feature under test, not the feature being tested.
Ready to stop paying the login tax on every run? npm install -g browserbash-cli and try browserbash auth save on your own staging environment, or sign up if you want the optional free cloud dashboard on top, an account is entirely optional.