Sync tabs in real time, no server required
Users routinely open web apps in several browser tabs at once. The Broadcast Channel API elegantly solves the resulting synchronization problem: logout, theme switching, session updates and state changes are sent instantly to every open tab, entirely without a server, without WebSocket, without polling.
Table of Contents
- 1. The multi-tab problem in web applications
- 2. BroadcastChannel: core principle and API
- 3. Practical example: secure multi-tab logout
- 4. Syncing theme and settings across tabs
- 5. Reactive state sharing between tabs
- 6. Service workers and BroadcastChannel
- 7. Structured messages and protocol design
- 8. Error handling and cleanup
- 9. BroadcastChannel vs. alternatives
- 10. Summary
- 11. FAQ
1. The multi-tab problem in web applications
Modern web apps are frequently opened by users in several browser tabs at the same time. This creates a classic synchronization problem: if the user logs out in tab A, tab B stays logged in and keeps displaying private data. If the user updates their shopping cart in tab A, tab B still shows the stale cart. If a session expires, some tabs still show it as active while others already display error messages. Without communication between tabs, these inconsistencies are unavoidable.
The traditional approaches all have drawbacks. localStorage events (the storage event) only fire when another tab writes, not in the writing tab itself, and offer no structured message protocol. SharedWorker as a central communication hub is more powerful but noticeably more complex. Server-Sent Events or WebSockets require server infrastructure for a problem that is purely client-side. The Broadcast Channel API is the direct, native solution: a named channel on which any number of browsing contexts of the same origin can send and receive messages.
2. BroadcastChannel: core principle and API
The Broadcast Channel API is simple: you create a channel with a name, register an onmessage handler, and can send messages on the same channel with postMessage(). Every other browsing context of the same origin that has a channel open with the same name receives the message, instantly, synchronously within the same event loop turn of the receiving context. The sending context never receives its own message back.
The channel name is the only form of addressing. Tabs on https://app.mironsoft.de and https://www.mironsoft.de do not share a channel even if both use the same channel name, because the origins differ. Tabs on https://app.mironsoft.de/page1 and https://app.mironsoft.de/page2 share the same channel because the origin is identical. A BroadcastChannel must be explicitly closed with channel.close() once it is no longer needed, to avoid memory leaks.
// Tab A and Tab B share the same origin, they communicate via named channel
// --- Tab A (sender) ---
const channelA = new BroadcastChannel('app-events');
// Send structured message (any serializable value works)
channelA.postMessage({
type: 'USER_LOGGED_OUT',
userId: 'u-123',
timestamp: Date.now(),
});
// Tab A does NOT receive its own message
// --- Tab B (receiver, any other tab on same origin) ---
const channelB = new BroadcastChannel('app-events'); // same name = same channel
channelB.onmessage = (event) => {
const { type, userId, timestamp } = event.data;
console.log(`Received event: ${type} for user ${userId}`);
if (type === 'USER_LOGGED_OUT') {
// Redirect or clear state in this tab
window.location.href = '/login';
}
};
// Always close channels when done to prevent memory leaks
window.addEventListener('beforeunload', () => {
channelA.close();
channelB.close();
});
3. Practical example: secure multi-tab logout
The most common and most critical use case for the Broadcast Channel API is logout synchronization. When a user logs out of a business application, every open tab must react immediately: hide private data, clear forms, redirect to the login page. A tab that still shows access to sensitive data after logout is a security problem. With the Broadcast Channel API this is solvable in a few lines.
A robust logout system built with BroadcastChannel does not just send a simple signal on logout, but a structured object with a type, a timestamp and an optional reason. The receiver checks the type, cleans up local state (resetting state management, removing localStorage entries, aborting pending requests) and then redirects. The same mechanism works for session timeouts: the service worker, which periodically checks session validity, sends a session-expired event on the BroadcastChannel, and every tab reacts simultaneously.
// auth-sync.js: multi-tab authentication synchronization
const AUTH_CHANNEL = 'auth-events';
class AuthSync {
#channel;
#handlers = new Map();
constructor() {
this.#channel = new BroadcastChannel(AUTH_CHANNEL);
this.#channel.onmessage = this.#handleMessage.bind(this);
this.#channel.onmessageerror = (e) => console.error('BroadcastChannel error:', e);
}
// Send logout to all other tabs
broadcastLogout(reason = 'user_initiated') {
this.#channel.postMessage({
type: 'LOGOUT',
reason,
timestamp: Date.now(),
});
// Also perform logout in current tab
this.#performLogout(reason);
}
// Register typed event handlers
on(type, handler) {
this.#handlers.set(type, handler);
return this;
}
#handleMessage({ data }) {
const handler = this.#handlers.get(data.type);
if (handler) handler(data);
}
#performLogout(reason) {
sessionStorage.clear();
localStorage.removeItem('auth_token');
window.location.replace(`/login?reason=${reason}`);
}
destroy() {
this.#channel.close();
}
}
// Usage
const authSync = new AuthSync();
authSync.on('LOGOUT', ({ reason }) => {
console.log('Logout received from another tab:', reason);
sessionStorage.clear();
window.location.replace('/login?reason=remote_logout');
});
4. Syncing theme and settings across tabs
Another classic use case is synchronizing user preferences. When a user turns on dark mode in tab A, every other open tab of the same app should switch to dark mode immediately, without a page reload and without a server round trip. The same applies to language settings, font sizes, dashboard layouts and other preferences that should be applied in real time.
The BroadcastChannel pattern for settings is bidirectional: when tab A changes a setting, it sends it on the channel. Every other tab receives the change and applies it. Tab A itself applies the change directly (it does not send it to itself). This is more efficient than the storage-event approach, which always has to write to localStorage first before other tabs receive the event. With BroadcastChannel the message is transferred directly in memory, no storage round trip involved.
5. Reactive state sharing between tabs
More complex applications, especially collaboration tools, dashboards and e-commerce applications, benefit from deeper state synchronization across tabs. Instead of individual events, you send state patches or state snapshots over the BroadcastChannel. A user adds a product to the cart in tab A, and tab B immediately shows the updated cart indicator in the header, with no server polling involved.
A resilient state-sharing protocol over BroadcastChannel sends delta objects instead of full state copies to save bandwidth. New tabs that open and join a channel cannot receive an initial state (the channel has no history). This problem is solved by having a new tab send a REQUEST_STATE event, to which an existing tab replies with the current state. This handshake pattern makes state sharing over BroadcastChannel robust even for late-joining tabs.
// State synchronization with late-join support via REQUEST_STATE handshake
const STATE_CHANNEL = 'app-state';
const channel = new BroadcastChannel(STATE_CHANNEL);
let localState = { cart: [], theme: 'light', notifications: 0 };
// New tab requests state snapshot from existing tabs
channel.postMessage({ type: 'REQUEST_STATE' });
channel.onmessage = ({ data }) => {
switch (data.type) {
case 'REQUEST_STATE':
// Respond with current state (one existing tab will answer)
channel.postMessage({ type: 'STATE_SNAPSHOT', state: localState });
break;
case 'STATE_SNAPSHOT':
// Only apply if we don't have state yet (first snapshot wins)
if (!localState._initialized) {
localState = { ...data.state, _initialized: true };
renderApp(localState);
}
break;
case 'STATE_PATCH':
// Apply partial update from another tab
localState = { ...localState, ...data.patch };
renderApp(localState);
break;
}
};
// Dispatch state update, applies locally AND broadcasts to other tabs
function updateState(patch) {
localState = { ...localState, ...patch };
renderApp(localState);
channel.postMessage({ type: 'STATE_PATCH', patch });
}
function renderApp(state) {
// Re-render relevant parts of the UI
document.body.dataset.theme = state.theme;
}
6. Service workers and BroadcastChannel
The Broadcast Channel API works not only between tabs but also between service workers and the tabs they control. This opens up powerful scenarios: a service worker can receive push notifications and forward the relevant data directly over the BroadcastChannel to every open tab, without addressing each tab individually via clients.matchAll(). This is simpler and easier to maintain than the alternative Client.postMessage() API.
A practical example: the service worker receives a push event (a new message for the user), updates the cache, and then sends an event on the BroadcastChannel. Every open tab receives the event and updates its unread counter or shows an in-app toast. Without BroadcastChannel, the service worker would have to enumerate every client and notify each one individually: more code, more failure points, for the same functionality.
7. Structured messages and protocol design
The most common mistake in BroadcastChannel implementations is a missing message protocol. Without clear typing, receivers turn into monolithic switch statements full of magic strings nobody understands anymore. A robust protocol defines all message types as constants or TypeScript discriminated unions, always includes a timestamp and an optional sender ID, and versions the protocol whenever the format changes.
Sending large objects over BroadcastChannel is possible: the browser internally uses the Structured Clone Algorithm, which correctly copies TypedArrays, Map, Set, Date and other complex types. Functions and DOM elements, however, cannot be transferred. For very large data (such as images or large arrays), SharedArrayBuffer is the better choice, since BroadcastChannel copies the data instead of sharing it. The BroadcastChannel protocol should therefore stay limited to control messages and small data payloads.
8. Error handling and cleanup
The Broadcast Channel API has an onmessageerror event, which fires when a message is received that cannot be deserialized, for example when the Structured Clone Algorithm encounters a non-cloneable type. In practice this is rare, but it should still be handled, especially when different tabs or service workers might run different code versions.
BroadcastChannel instances keep an open port in the browser. If you forget to call channel.close(), the port stays open, even for a short time after the page unloads. In single-page applications, cleanup needs particular attention: in React components you close the channel in the useEffect cleanup function. In Vue components in onUnmounted. The beforeunload event is a safety-net option, but not a reliable cleanup method, since it does not fire reliably in every browser.
9. BroadcastChannel vs. alternatives
The right choice of tab communication method depends on the use case. The Broadcast Channel API is the most modern and most direct solution for most scenarios. It is simpler than SharedWorker, more flexible than localStorage events and requires no server infrastructure like WebSockets.
| Method | Receivers | Limitations | Recommendation |
|---|---|---|---|
| BroadcastChannel | All tabs, SW, iframes (same-origin) | Same-origin only | Standard choice for tab sync |
| localStorage storage event | Other tabs (not sender) | Strings only, sender excluded | Legacy, prefer BroadcastChannel |
| SharedWorker | All tabs (centralized) | More complex, no Safari before 16 | When central state is needed |
| WebSocket | Server + all clients | Server infrastructure required | When server push is needed |
| Service Worker postMessage | Individual clients | Complex client management | BroadcastChannel from within the SW |
The Broadcast Channel API is available in every modern browser (Chrome 54+, Firefox 38+, Safari 15.4+) and in Node.js since version 15.4. For projects that still need to support older Safari versions, a polyfill or a fallback to localStorage events is necessary. In practice, Safari versions below 15.4 (released October 2021) are rarely still a relevant target audience for professional web applications.
Mironsoft
Frontend architecture, multi-tab state and web app development
Want to secure your web app with multi-tab synchronization?
We implement robust tab synchronization with the Broadcast Channel API for logout, session management and real-time state updates, without WebSocket overhead.
Security audit
Analysis of multi-tab security gaps: logout without tab sync, session leaks in other tabs
Implementation
BroadcastChannel integration for auth, state sync and real-time settings in React or Vue
Service Worker
Connecting a service worker with BroadcastChannel for push notifications and offline sync
10. Summary
The Broadcast Channel API solves the multi-tab synchronization problem with minimal effort: a named channel, a postMessage() call, an onmessage handler. Every browsing context of the same origin, tabs, iframes, service workers, receives messages instantly. No server, no WebSocket, no polling. The API is available in every modern browser and, since Node.js 15.4, usable server-side as well. The Structured Clone Algorithm enables transferring complex objects, no JSON serialization required.
The most important use cases are logout synchronization (security-critical), theme and settings sync (UX quality), session-timeout notifications (session management) and reactive state sharing (collaboration features). For state sharing with late-join support, implement a REQUEST_STATE/STATE_SNAPSHOT handshake protocol. Do not forget channel.close(), every BroadcastChannel instance must be closed explicitly. With these fundamentals, the Broadcast Channel API is a powerful tool for professional multi-tab web applications.
Broadcast Channel API, the essentials at a glance
Core principle
new BroadcastChannel('name') opens a channel. postMessage() sends to every other context of the same origin. onmessage receives. close() cleans up.
Scope
All tabs, iframes and service workers of the same origin receive messages. Cross-origin does not work. The sender does not receive its own message.
Main use cases
Logout synchronization (security), theme/settings sync (UX), session-timeout propagation, reactive state sharing between tabs.
Browser support
Chrome 54+, Firefox 38+, Safari 15.4+, Node.js 15.4+. For older Safari versions: polyfill or localStorage events as a fallback.