Skip to content
WhatsApp
AI StudioPricing
Development16 min read2026-09-09

Cross-Tab State Synchronization: BroadcastChannel API, LocalStorage Events, and SharedWorkers

Keep multiple browser tabs in perfect sync. Master cross-tab communication using the native BroadcastChannel API, storage event fallbacks, multi-tab leader election, and synchronized authentication states.

Marcus Vance
Staff Infrastructure Engineer
Cross-Tab State Synchronization: BroadcastChannel API, LocalStorage Events, and SharedWorkers

The Multi-Tab Nightmare in Modern Web Applications

Users routinely open multiple tabs of the same SaaS application: they might keep a dashboard open in Tab 1, edit a customer profile in Tab 2, and review a report in Tab 3.

Without cross-tab state synchronization, awkward bugs multiply: logging out in Tab 1 leaves sensitive financial data visible in Tab 2. Adding an item to a shopping cart in Tab 3 fails to update the header badge in Tab 1. Worse, concurrent writes from two tabs create silent overwrite collisions.

The modern web platform provides elegant primitives to solve this problem: the BroadcastChannel API for high-velocity peer messaging, window storage events for legacy fallbacks, and leader election algorithms to designate a single active coordinator tab.

In this guide, we explore the architecture of real-time cross-tab state synchronization in production applications.

Pro Tip: Use BroadcastChannel for cross-tab messaging. It is significantly faster than localStorage events because it does not serialize to physical disk.

The BroadcastChannel API: Clean Same-Origin Messaging

The BroadcastChannel interface enables bidirectional communication between all browser windows, tabs, iframes, and Web Workers sharing the same origin (protocol, domain, and port).

Unlike postMessage, you do not need reference handles to target windows; every listener subscribed to the named channel receives transmitted payloads automatically.

authChannel.ts (Instant Multi-Tab Logout)
typescript
// Define typed broadcast message payloads
export type AuthBroadcastMessage =
  | { type: "AUTH_LOGIN"; userId: string; token: string }
  | { type: "AUTH_LOGOUT" }
  | { type: "THEME_CHANGED"; theme: "dark" | "light" };

export class CrossTabMessenger {
  private channel: BroadcastChannel | null = null;

  constructor(channelName: string, private onMessage: (msg: AuthBroadcastMessage) => void) {
    if (typeof window !== "undefined" && "BroadcastChannel" in window) {
      this.channel = new BroadcastChannel(channelName);
      this.channel.onmessage = (event: MessageEvent<AuthBroadcastMessage>) => {
        this.onMessage(event.data);
      };
    }
  }

  public post(message: AuthBroadcastMessage): void {
    this.channel?.postMessage(message);
  }

  public destroy(): void {
    this.channel?.close();
  }
}

Cross-Tab Leader Election: One Tab to Rule Them All

When five tabs are open, should all five maintain open WebSocket connections to the backend server? Doing so multiplies server connection loads and database socket overhead by 5x.

A superior architecture designates one tab as the 'Leader' (Coordinator). Only the Leader maintains the live WebSocket connection and background sync timers. The remaining follower tabs communicate with the Leader locally over the BroadcastChannel.

If the user closes the Leader tab, the remaining tabs conduct an automated leader election within 200ms using heartbeat leases and monotonic timestamp voting.

leaderElection.ts (Bully / Heartbeat Election Algorithm)
typescript
export class LeaderElection {
  private isLeader = false;
  private channel: BroadcastChannel;
  private tabId = Math.random().toString(36).substring(2, 9);
  private heartbeatInterval: any = null;
  private leaderExpiryTimeout: any = null;

  constructor() {
    this.channel = new BroadcastChannel("tab_leadership");
    this.channel.onmessage = (e) => this.handleMessage(e.data);
    this.claimLeadership();
  }

  private handleMessage(msg: any) {
    if (msg.type === "LEADER_PULSE") {
      // Leader is alive; reset expiration timer
      clearTimeout(this.leaderExpiryTimeout);
      this.leaderExpiryTimeout = setTimeout(() => {
        console.warn("Leader died. Stepping up to claim leadership.");
        this.claimLeadership();
      }, 3000);
    }
  }

  private claimLeadership() {
    this.isLeader = true;
    this.channel.postMessage({ type: "LEADER_PULSE", leaderId: this.tabId });

    this.heartbeatInterval = setInterval(() => {
      if (this.isLeader) {
        this.channel.postMessage({ type: "LEADER_PULSE", leaderId: this.tabId });
      }
    }, 1000);
  }

  public getStatus() {
    return { isLeader: this.isLeader, tabId: this.tabId };
  }
}

Storage Event Fallbacks for Legacy Environments

In older embedded WebViews or legacy browsers lacking BroadcastChannel, window.addEventListener('storage') serves as a reliable fallback.

Note that the storage event only fires in tabs OTHER than the tab that made the localStorage modification. Writing to localStorage triggers synchronous disk writes, so reserve it strictly as a fallback.

React Hook Integration: useCrossTabState

Encapsulate cross-tab communication into a custom React hook that syncs component state across tabs transparently, mirroring useState mechanics with zero configuration overhead.

useCrossTabState.ts (Synchronized React State)
tsx
import { useState, useEffect, useRef } from "react";

export function useCrossTabState<T>(key: string, initialValue: T): [T, (val: T) => void] {
  const [state, setState] = useState<T>(initialValue);
  const channelRef = useRef<BroadcastChannel | null>(null);

  useEffect(() => {
    channelRef.current = new BroadcastChannel(`tab_state_${key}`);
    channelRef.current.onmessage = (e: MessageEvent<T>) => {
      setState(e.data);
    };

    return () => {
      channelRef.current?.close();
    };
  }, [key]);

  const setCrossTabState = (newValue: T) => {
    setState(newValue);
    channelRef.current?.postMessage(newValue);
  };

  return [state, setCrossTabState];
}

Security Considerations: Origin Isolation

BroadcastChannel broadcasts to all documents on the exact same origin. If your application hosts untrusted user content on subdomains or shared paths, ensure strict Cross-Origin-Opener-Policy (COOP) and origin isolation to prevent malicious iframes from listening to broadcast channels.

Web Locks API (navigator.locks) for Cross-Tab Mutual Exclusion

While BroadcastChannel handles event notification, coordinating exclusive mutations (such as writing to IndexedDB or executing payments) requires mutual exclusion (mutexes).

The native Web Locks API coordinates resource access across tabs with zero polling. Request an exclusive lock; if another tab currently holds it, your callback automatically queues until the active tab releases the lock.

webLocks.ts (Cross-Tab Mutex Execution)
typescript
export async function runExclusiveTask(lockName: string, task: () => Promise<void>) {
  if ("locks" in navigator) {
    await navigator.locks.request(lockName, { mode: "exclusive" }, async (lock) => {
      console.log(`Exclusive lock acquired: ${lock.name}`);
      await task();
    });
  } else {
    // Fallback execution without browser-level mutex
    await task();
  }
}
Frequently Asked Questions

Does BroadcastChannel work between different domain origins?

No. For security reasons, BroadcastChannel strictly adheres to the Same-Origin Policy (same protocol, domain, and port).

What is the message size limit for BroadcastChannel payloads?

BroadcastChannel uses the structured clone algorithm. It can transmit complex objects, typed arrays, and Blobs up to dozens of megabytes, though lightweight payloads under 64KB are recommended for speed.

How does SharedWorker compare to BroadcastChannel?

SharedWorker runs a single background thread shared across tabs, making it ideal for maintaining a single shared database connection or WebSocket. However, Safari's support for SharedWorker has historically been inconsistent.

What happens if a leader tab crashes or freezes?

Heartbeat leases expire within 2-3 seconds, prompting follower tabs to trigger an automated election and appoint a new leader seamlessly.

How should authentication token refresh be coordinated across tabs?

Only the designated Leader tab should make the HTTP POST refresh call to /api/auth/refresh. Once the new token arrives, broadcast it to all follower tabs to prevent token refresh race conditions.

How does the Web Locks API prevent deadlocks if a tab is abruptly closed?

The browser runtime automatically releases all locks held by a tab when the tab terminates or crashes, guaranteeing zero permanent deadlocks.

Topics:#BroadcastChannel API#Cross-Tab Sync#Multi-Tab State#Leader Election#SharedWorkers#Auth Synchronization#Web Messaging#State Management

Want this thinking applied to your product?

Spin up a prototype in minutes, or brief our team and get a scoped plan within 24 hours.