AuthGENERALBROWSER-SPECIFICPLATFORM-SPECIFIC

Auth Across Tabs

One session, several documents, no shared memory: logging out in one tab has to reach the others, and the tab that missed the message is still rendering a logged-in UI.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

The user has five tabs open and logs out in one of them — what happens in the other four, and what should?

The user intent

A person on a shared machine clicks Sign Out and walks away. They believe they have left. Every tab they opened this morning is part of that belief.

The obvious build

Logging out clears the credential and redirects this tab to the login page. The credential is shared, so the other tabs are logged out too — they will find out on their next request.

Why it breaks

"They will find out on their next request" can mean never. A tab showing a rendered dashboard makes no further requests until something prompts it, so it sits there displaying the user's data indefinitely on an unattended screen.

How it breaks in a real browser
  • "They will find out on their next request" can mean never. A tab showing a rendered dashboard makes no further requests until something prompts it, so it sits there displaying the user's data indefinitely on an unattended screen.
  • If the credential lives in memory rather than in shared storage, the other tabs are not logged out at all. Each document has its own module scope, so clearing a variable in one tab clears nothing anywhere else (Cookies vs Script-Readable Tokens).
  • The reverse case is worse in a different way: a user logs in as a different account in a second tab. The first tab now renders one identity's UI over another identity's session, and the next write goes to the wrong account.
  • A background poll or a live connection in a stale tab keeps working until the server refuses it, and then usually reconnects, because reconnection logic rarely distinguishes "connection dropped" from "you are not welcome any more" (Reconnect and Backoff).
  • Tabs are not the only participants. A service worker, a shared worker and an installed PWA window all hold their own view of the session, and none of them observe a variable being cleared (The Service Worker Lifecycle).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Each tab is a separate document with a separate JavaScript realm and separate memory. Two tabs on the same origin share the cookie jar, Web Storage, IndexedDB and Cache Storage — and share nothing else (Choosing Browser Storage).
  • The storage event fires on other same-origin documents when Web Storage changes, never on the one that made the change. That asymmetry is the feature: it is a notification channel, and it is why the writing tab must also handle its own transition locally.
  • BroadcastChannel is the explicit version: a named channel that any same-origin context — tabs, iframes, workers, service workers — can post structured messages on and receive them from. It is the right tool when you want to send an event rather than piggyback on a storage write (State Synchronization).
  • Cookies are shared but silent. Nothing fires when a cookie changes, so a cookie-based session that ends in one tab is invisible to the others until something makes a request. This is the single most common reason stale tabs exist (Cookies).
  • The Page Visibility and focus events are the pragmatic backstop: when a tab becomes visible again, re-check identity. It is not a real-time channel, but it covers the case that matters most — the moment the user looks at the stale tab (Long-Lived Clients and Version Skew).
  • A stale tab is a version-skew problem in miniature: two clients holding different beliefs about shared state, with no coordination protocol between them beyond what you build (Who Owns This State?).

What this makes the browser do

And which of it is avoidable.

  • A storage event is delivered to every other same-origin document, and each handler runs on that document's main thread. A handler that re-renders the whole application does so in every open tab at once.
  • BroadcastChannel delivery is asynchronous and structured-cloned, so anything you post is copied per receiver. Post identifiers and event names, not application state (Structured Clone and Transferables).
  • Background tabs are throttled. A logout message posted while a tab is hidden is delivered, but any timer-driven work the handler schedules may not run until the tab is visible again (Tasks: The Unit That Cannot Be Interrupted).
  • Every tab that reacts by refetching produces a simultaneous burst of requests from one user. Coordination avoids the stampede your coordination mechanism otherwise causes (Thundering Herd in Concurrency & Parallelism).

Same origin, same storage, separate minds

Two tabs on the same origin share every persistent store the browser offers and share no memory at all. That single sentence explains almost every cross-tab auth bug: the credential is common, the rendered belief about it is not, and there is no event that reconciles the two unless you create one.

Notice which of the shared stores notify you. Web Storage does, on other documents. BroadcastChannel does, because that is all it is. Cookies do not, IndexedDB does not, and Cache Storage does not — so a cookie-session application has, by default, no cross-tab awareness whatsoever.

A logout that reaches every tab, and a receiver that does not destroy anyone's work
1const channel = new BroadcastChannel('auth')
2
3export async function logout() {
4 await api.post('/auth/logout') // the server ends the session — this is the real part
5 clearInMemoryCredential()
6 await caches.delete('authed-responses')
7 queryCache.clear()
8 // Tell everyone else. This tab does NOT receive its own message.
9 channel.postMessage({ type: 'logout', sessionId: currentSessionId() })
10 identity.set({ status: 'anonymous' })
11 navigateToLogin() // only THIS tab navigates
12}
13
14channel.onmessage = (event) => {
15 const msg = event.data
16 // Ignore a message from a session this tab has already moved past.
17 if (msg.sessionId && msg.sessionId !== currentSessionId()) return
18
19 if (msg.type === 'logout') {
20 clearInMemoryCredential()
21 stopPolling()
22 closeLiveConnection() // or it will reconnect its way back in
23 identity.set({ status: 'anonymous' })
24 announce('You signed out in another tab.') // polite live region
25 // Deliberately no navigation: this tab may hold an unsaved draft.
26 }
27}
28
29// Backstop for anything the message never reached — a frozen tab, a
30// discarded one, a cookie session ended by the server on its own.
31document.addEventListener('visibilitychange', () => {
32 if (document.visibilityState === 'visible') revalidateIdentity()
33})

Three decisions carry the lesson: the server call happens first and is the only part that actually ends anything, the receiver does not navigate, and visibilitychange covers every tab the message failed to reach.

Who else is holding a session

It is tempting to think of this as a tab problem, and then to be surprised by the participant that kept working. Any same-origin context can hold session state: other tabs, iframes, a shared worker, a service worker that outlives all of them, and an installed app window that the user does not think of as a tab at all.

The service worker deserves particular attention, because it sits between every one of those contexts and the network. If it has cached authenticated responses, it can serve the previous user's data to the next one from a cache hit, with no request ever reaching your server to be refused (Caching Strategies).

  • Other tabs — the case everybody thinks of, and the easiest to fix.
  • Same-origin iframes — they receive the message and have their own rendered state (Origins and the Sandbox).
  • Shared and dedicated workers — they hold their own copies of anything they were given (Talking to a Worker).
  • The service worker — outlives every document and can answer from cache without touching the network (Intercepting Fetch).
  • An installed PWA window — a separate context the user does not perceive as a tab (Manifest and Installability).
  • A frozen or discarded tab that will be restored later from a state predating the logout — the one the broadcast cannot reach.
One origin, several contexts, one logout
1. the only real logout2. broadcastclears + announcessame origin, same channelmust be purgedmay never arrive3. backstop on returnTab A — user clicks Sign Outvisibilitychange -> revalidateServer ends the sessionBroadcastChannel("auth")Tab B — rendered dashboardTab C — frozen in backgroundInstalled app windowService worker — outlives every tabCache Storage — authenticated responses
UserLLMAgentToolDataDecisionHumanGuardrail

The tab that missed the message

Every cross-tab design has a hole, because delivery is best effort and documents get frozen, discarded and restored. Planning for the tab that missed the message is not defensive over-engineering; it is the normal case on a phone, where a background tab may be discarded within minutes and restored an hour later from a snapshot.

The backstop is cheap: on becoming visible, ask the server who this is. It converts an unbounded window of staleness into one bounded by "the moment the user looked at it", which is precisely the moment it matters (State Synchronization).

Cross-tab auth failures and what each one actually needs
TriggerSymptomCauseResponse
Logout in tab A, cookie sessionTabs B and C keep rendering data indefinitelyCookie changes fire no event; nothing prompted the other tabsBroadcast the transition explicitly, and revalidate on visibilitychange for the ones it did not reach.
Logout received in a background tabFour tabs navigate to login; four drafts destroyedThe receiver treated a notification as a navigation triggerClear state and render in place. Only the tab where the user acted should navigate (Form State Is a Draft).
Login as a different user in tab BTab A writes succeed against the wrong accountTab A's rendered identity and the shared credential no longer matchCarry a session identifier; on mismatch, treat it as a full reset rather than a refresh.
Tab restored from a discarded stateA fully rendered authenticated UI with no valid sessionThe tab predates the logout and received no messageRevalidate identity on becoming visible; render from the answer, not from the snapshot.
Live connection in a stale tabTraffic continues after logout, then reconnects in a loopReconnect logic does not distinguish a dropped connection from a refused oneClose on logout and check identity before every reconnect attempt (Reconnect and Backoff).
Service worker serving cached authenticated responsesThe next user of the device sees the previous user's dataLogout cleared page state but not Cache StoragePurge authenticated caches as part of logout, in the service worker as well as the page (Cache Storage).
T0  Tab A, Tab B, Tab C open and authenticated as user X
T1  Tab C is hidden -> browser freezes it, then discards it
T2  Tab A: user signs out
      -> POST /auth/logout          session invalidated server-side
      -> BroadcastChannel: {logout} delivered to Tab B
      -> Tab A navigates to /login
T3  Tab B: clears credential, stops polling, renders signed-out state,
      announces "You signed out in another tab." No navigation: the
      half-written reply in its composer is still there.
T4  Tab C: gone. It received nothing. It never will.
T5  User returns to Tab C an hour later
      -> browser restores it from a snapshot taken at T1
      -> pixels show user X's dashboard, fully rendered
      -> visibilitychange fires -> revalidateIdentity()
      -> server: no session -> identity = anonymous
      -> Tab C renders signed-out state and announces it

Without T5, Tab C displays user X's data until someone closes it.

How to build it

Most important first.

  • Broadcast the identity transition explicitly. BroadcastChannel for the event, with a storage-event fallback where you need the widest reach, and treat both as notifications that trigger a re-check rather than as the source of truth (State Synchronization).
  • Make the receiving behaviour correct rather than dramatic. A tab receiving a logout should clear in-memory identity, stop background work and render an unauthenticated state — not navigate, because navigating four background tabs destroys four sets of unsaved work (Form State Is a Draft).
  • Re-verify with the server on the transition, and on visibilitychange when a tab becomes visible. A message is a hint that something changed; the server is what says what is true.
  • Include an identity discriminator in the message and in fetched data. If a tab discovers that the current session belongs to a different user than the one it rendered, that is a full reset, not a refresh.
  • Stop live connections and polls on logout in every tab, and make reconnection logic check identity before reconnecting, or a stale tab will reconnect its way back to life (WebSockets in the UI).
  • Tell the receiving tab what happened. A tab that silently empties looks broken; "You signed out in another tab" is a complete explanation and costs one sentence (Live Regions and Announcement).
  • Ignore messages from a channel as authority for logging in. A broadcast can only be produced by same-origin code, but treating a message as proof of a session invites a bug where an unauthenticated tab renders an authenticated UI it has no credential for.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • A tab that changes state while the user is not looking at it must announce what happened when they return. Silence plus a changed screen is the most disorienting possible outcome for a screen-reader user (Live Regions and Announcement).
  • Do not move focus in a background tab. A focus change in a document the user is not currently in can be picked up by assistive technology as an unexpected context switch when they do return.
  • When a tab transitions to unauthenticated in place, focus must end up somewhere meaningful — the explanation, or the sign-in control. Focus on a removed node lands on the document body with no announcement (Focus Management).
  • Change the document title when the tab's state changes. The title is how screen-reader and switch users identify tabs, and a tab still titled "Inbox — 12 unread" that is now signed out is actively misleading.
  • Any "you have been signed out" surface is content, not decoration: reachable in the accessibility tree, keyboard-operable, and not conveyed by a colour change alone (Semantics Before ARIA).

What can go wrong

Failure modes
  • The stale tab: rendered, credential-less, and still showing the user's data on an unattended screen because nothing prompted it to check.
  • Broadcast implemented, receiver navigates: four background tabs jump to a login page and every draft in them is gone.
  • Identity confusion: tab A is user X, tab B logs in as user Y, tab A writes with Y's session against X's rendered view, and the write succeeds against the wrong account.
  • A logout race: two tabs both notice expiry and both attempt refresh or logout, producing duplicate calls and, with credential rotation, invalidating each other (Session Expiry and the Refresh Race).
  • The mitigation failing: a storage listener that reacts to every write, so an unrelated key written on each keystroke re-renders every open tab.
  • A service worker that keeps serving cached authenticated responses to a tab that has been logged out, because the cache was never cleared as part of the logout path (Cache Storage).
  • Logout that only clears client state. If the server session is still valid, a copy of the credential taken earlier still works — clearing the browser is cosmetic (Sessions in Security Engineering).
What can arrive out of order
  • Logout in one tab and a request already in flight in another: the request may be authorized against a session that is being terminated, and may succeed or fail depending on which reaches the server first.
  • Two tabs both detecting expiry and both refreshing. With credential rotation, the second invalidates the first and the user is logged out by two correct implementations disagreeing (Session Expiry and the Refresh Race).
  • A login as a different user racing a message from a previous session: a tab can receive a stale logout notification just after a new session was established, and log out a session that had only just begun. Carry a session identifier in the message and ignore ones that do not match.
  • A tab restored from a frozen state after a logout it never received, rendering a UI from before the transition (Long-Lived Clients and Version Skew).
Security
  • The browser enforces the origin boundary on all of these channels: storage events, BroadcastChannel and shared storage are same-origin only, so another site cannot listen in or post to them (The Same-Origin Policy).
  • The browser enforces nothing about what your tabs believe. Cross-tab logout is a convention you implement, and a tab that misses the message keeps its rendered UI (The Browser Security Model).
  • Logout must invalidate the session server-side. Everything a tab does — clearing memory, deleting a cookie, wiping storage — is local, and a credential captured earlier is unaffected by any of it (Sessions in Security Engineering).
  • On a shared device, the stale tab is the actual risk this lesson exists for: the user believes they have left and the screen says otherwise.
  • Never broadcast credentials. Post the fact that identity changed, not the token — a message is copied to every same-origin context including iframes and workers you may not have audited (Third-Party Scripts and the Supply Chain).
  • Clear caches that hold authenticated responses on logout, including Cache Storage and any in-memory query cache, or the next user of the machine gets the previous one's data from a cache hit (Storage Security and Durability).
Misreads
  • "They share the cookie, so they share the session state." They share the credential. What each tab has rendered is separate memory and stays exactly as it was.
  • "The storage event will fire for me too." It fires on other documents. The tab that wrote the value must handle its own transition directly.
  • "Clearing storage logs the user out." It logs out this browser profile's client state. The server session is unaffected until the server is told (Sessions in Security Engineering).
  • "BroadcastChannel is insecure because anything can listen." It is same-origin only. The real caution is that same-origin includes iframes and third-party script running in your page (Third-Party Scripts and the Supply Chain).
  • "We redirect all tabs to login, so it is handled." You destroyed unsaved work in four tabs to solve a problem that a rendered explanation solves without losing anything.

Measuring it, and what changes in the field

How you would see this
  • Open two tabs, log out in one, and watch the other without touching it. This is the whole test and almost nobody runs it (End-to-End Testing).
  • Application panel in the second tab: are the cookie and storage entries gone, and is the query cache still holding authenticated responses? (Debugging State)
  • Network panel in a stale tab: a live connection that reconnects after logout is visible immediately as a repeating upgrade request (Debugging the Network).
  • In the field, look for requests arriving with a session id that was terminated. A steady trickle is stale tabs, and the volume tells you how big the problem is (Frontend Error Tracking).
Slow device, slow network, large data, old tab
  • With a cookie-based session, nothing fires on change, so the other tabs are silent by default and an explicit channel is mandatory rather than optional.
  • With an in-memory credential, tabs are fully independent and cross-tab logout is entirely something you build; there is no shared value to clear.
  • On mobile, background tabs are frozen or discarded, so messages may be delivered to a document that is about to be destroyed — and the tab may be restored later from a state that predates the logout.
  • In an installed PWA, a window and a browser tab on the same origin are separate contexts that share storage; both need to participate (Manifest and Installability).
  • With a service worker in the middle, there is a sixth participant that outlives every tab and must be told too (Intercepting Fetch).
What this costs
  • Broadcasting adds a channel, a message contract and a handler in every tab — a real amount of coordination code for a case that many teams never test.
  • Re-checking identity on every visibilitychange costs a request each time a user switches back to the tab. Debounce it, or a heavy tab-switcher generates a request per switch.
  • Reacting in place rather than navigating preserves the user's work and means a tab can be sitting in a partially authenticated view that must be safe to render without a session — which is more states to design.
  • A shared worker centralises session state elegantly and adds a lifecycle to reason about, and it is one more thing to keep alive across browsers that treat it differently.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALOne realm per document, shared origin-scoped storage, and no automatic notification for cookies are true of every browser. The consequence — that cross-tab logout is something you implement rather than something you get — follows from the model rather than from any engine.
  • BROWSER-SPECIFICDelivery of storage events and BroadcastChannel messages to hidden, frozen or discarded tabs differs by browser and by platform, and mobile browsers freeze background documents far more aggressively than desktop ones. Treat a broadcast as best-effort and re-verify on visibility rather than assuming every tab received it.
  • PLATFORM-SPECIFICAn installed PWA window, a browser tab and a web view embedded in a native app can be separate contexts that may or may not share a cookie jar and storage partition depending on the platform. If you ship into more than one of these, verify the sharing rather than assuming it.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

API Designstatus-codes
Domains that do not exist yet
  • Distributed Systems — several tabs holding independent beliefs about one shared session is the same problem as replicas with no coordination protocol, and the remedies rhyme: broadcast the change, carry a version, and re-read from the authority when in doubt.
OS & Networkingipc-overview