In a browser, what problem does BroadcastChannel solve that posting to a specific worker or holding a MessagePort does not, and what are its delivery rules?
answer
- no handle to the other side
- a shared string, not a reference
- everyone same-origin on that name
- one participant is always skipped
- nothing is replayed for latecomers
basics
~20 sBroadcastChannel is same-origin fan-out by name rather than by reference: any context that opened a channel with the same name receives the message, with no handle to each other. The sending object never receives its own message, and there is no history for late joiners.
solid answer
~50 sEvery other messaging API needs a reference to the other end — a `Worker` object, a `MessagePort`, a window handle. `BroadcastChannel` needs only an agreed string: `new BroadcastChannel('auth')` in any same-origin tab, iframe, or worker joins that channel, and `channel.postMessage(value)` is delivered to all of them. That makes it the natural fit for cross-tab notification — "the user logged out", "this cache entry is stale" — where the sender neither knows nor cares who is listening. The rules to remember: delivery is to same-origin contexts only; the payload is structured-cloned as usual; the **sending object does not receive its own message**, so a tab that must also act on the event has to call its own handler directly; there is no replay, so a context created a second later hears nothing about what it missed; and `close()` detaches the object when you are finished. In embedded third-party contexts, modern browsers partition it by top-level site, so an iframe may not reach a same-origin top-level page.
code
javascript · 13 linesconst channel = new BroadcastChannel('auth');
channel.addEventListener('message', (event) => {
if (event.data.type === 'logout') location.reload();
});
function logout() {
clearSession(); // durable change first
channel.postMessage({ type: 'logout' }); // notify the other tabs
location.reload(); // this tab is NOT notified
}
window.addEventListener('pagehide', () => channel.close());go deeper
Know that BroadcastChannel lets same-origin tabs and workers talk by agreeing on a channel name, with no reference to each other, and that you listen for message events on the channel object.
State the delivery rules precisely: same origin, structured-cloned payloads, the sending object excluded from its own message, no replay for late joiners, and close() to detach.
Show the durable-store-plus-invalidation pattern — write the change, broadcast only what changed, let receivers re-read — and explain why carrying state in the message breaks the moment a tab is opened late or a message is missed.
Weigh it against a shared worker or a server channel when something must actually own state, arbitrate, or answer late joiners, and account for third-party partitioning before designing any cross-embedding protocol around it.
## Addressing by name instead of by reference Worker messaging and `MessagePort` are both *point to point*: you must be holding the other end. That is exactly right when there is a specific counterpart, and useless when there is not. Consider a user logging out in one tab of your app while three other tabs sit open. Those tabs have no references to each other. Nothing in the worker APIs helps. `BroadcastChannel` addresses by name: ```js const channel = new BroadcastChannel('auth'); channel.addEventListener('message', (event) => { if (event.data.type === 'logout') location.reload(); }); channel.postMessage({ type: 'logout' }); ``` Any same-origin context that has constructed a `BroadcastChannel` with the string `'auth'` receives that message as a `MessageEvent` — other tabs, iframes, dedicated workers, shared workers, a service worker. Nobody registered with anybody; the name is the rendezvous. ## The delivery rules **Same origin.** The channel is scoped to the origin. Another origin using the same name has its own, entirely separate channel. **The sender is excluded.** The message goes to every `BroadcastChannel` object on that name and origin *except the one that posted it*. This surprises people constantly, and the consequence is concrete: if the acting tab must also update itself, call the handling logic directly rather than expecting the broadcast to loop back. (Note the exclusion is per *object*, not per context — a second channel object with the same name in the same page would receive it.) **Payloads are cloned.** `postMessage` on a broadcast channel runs the same structured clone as everywhere else, so the same cost model and the same copy semantics apply. It is fan-out of a value, not shared state. **No history, no acknowledgement.** A context that joins the channel after a message was sent hears nothing about it, and the sender learns nothing about who received what. If a newly-opened tab needs current state, it must read it from storage or ask a server — broadcast is a notification mechanism, not a synchronization mechanism. **Ordering is per sender.** Messages from one sender arrive in the order posted; nothing coordinates across senders. **Lifecycle.** `channel.close()` detaches the object so it stops sending and receiving. Do it when the component or page area that owned it goes away; leaving channels open is a slow leak of live objects and handlers. **Partitioning.** In embedded third-party contexts, modern browsers partition same-origin communication APIs by top-level site, so a same-origin iframe inside another site is not necessarily on the same channel as your top-level tab. Do not design a cross-embedding protocol on this without testing it in each engine you support. ## The design pattern that actually works The pitfall is treating a broadcast as the transfer of truth. Two tabs both broadcasting their local state produces divergence the first time a message is missed or a tab is opened late, and there is no conflict resolution anywhere in the API. The robust shape is **one writer, then a notification**: the acting tab writes the new state to a durable place (IndexedDB, or the server), then broadcasts a small message saying *what changed* — not the new state itself. Receivers treat the broadcast purely as an invalidation signal and re-read from the durable source. Late joiners are then automatically correct, because they read the source on startup and never needed the message. Keeping the payload to a type tag and an identifier also sidesteps the clone cost entirely. ## Where it sits among the alternatives Against a **shared worker**, which several contexts connect to via ports: a shared worker gives you a single owner that can hold state, arbitrate, and answer late joiners — genuinely more powerful, and correspondingly more machinery, with weaker historical support across browsers. `BroadcastChannel` holds no state and arbitrates nothing. Against the **`storage` event**, which fires in other same-origin documents when `localStorage` changes: that is also a cross-tab signal, but it forces you to write through a synchronous storage API to send it, and its semantics are tied to storage rather than messaging. Where both are available, `BroadcastChannel` expresses the intent better. Against **`MessageChannel`**: use ports when there is a specific counterpart and the conversation is private; use a broadcast channel when the audience is open-ended and anonymous. Summarised: reach for `BroadcastChannel` when you want to say something to whoever is listening, keep the message small and declarative, and never let it be the only copy of anything that matters.
- A tab broadcasts a logout event and its own listener never fires. Is that a bug?No — it is the specified behaviour. A message is delivered to every BroadcastChannel object on that name and origin except the one that sent it. The acting tab must invoke its own handling directly. The exclusion is per channel object rather than per context, so a second channel object in the same page would in fact receive it.
- A tab opened just after a broadcast never learns about it. How do you make late joiners correct?Do not carry state in the message. Have the acting context write the change to a durable store or the server first, then broadcast a small invalidation naming what changed. Receivers re-read the source. A late joiner reads the same source on startup and is correct without having heard anything, which removes the entire class of missed-message bugs.
- When would a shared worker be the better choice than BroadcastChannel?When someone has to own state or arbitrate rather than merely notify — a single connection multiplexed across tabs, one poller instead of five, a queue with exactly-once semantics, or answering a late joiner with the current state. A shared worker is a stateful owner reached through ports; BroadcastChannel holds nothing and coordinates nothing.
saying these in an interview costs you the question
- Expects the sending tab to receive its own broadcast
- Assumes late-joining tabs get missed messages replayed
- Treats the broadcast payload as the source of truth
- Thinks it reaches other origins with the same channel name
- Never calls close(), leaving channels and handlers alive