Your Tabs Each Open Their Own WebSocket. navigator.locks Elects One.
Six open tabs means six live connections to your server, six duplicate pushes, six timers draining the battery. navigator.locks elects a single leader tab to hold the real connection — the rest just listen.

Open your app in six tabs — most people have at least that many of something open right now — and check your server's connection count. If you're using WebSockets or Server-Sent Events for live updates, it just went up by six. Six sockets, six keep-alives, six copies of every push notification landing in the same browser window.
Nobody asked for six connections. They asked to see their dashboard update live. You gave them that, and also a server bill and a battery drain that scale with how many tabs a bookmark-happy user happens to have open.
Open devtools, go to the Network tab, filter by WS, and open the same page twice. You'll see two independent socket connections, each sending its own ping, each receiving its own copy of every broadcast the server sends.
Now open six. Your server — which probably caps concurrent connections per deploy, or bills per connection-minute — just got six times the load from one human being. And when the server pushes a notification, that human sees it pop up six times, once per tab, because each tab's JavaScript has no idea the other five exist.
The obvious instinct is the Page Visibility API: pause the connection in background tabs, since the user is only looking at one of them anyway.
This helps a little and breaks something else. A user with the dashboard open on a second monitor, in a visible-but-not-active tab, stops getting updates. Worse, "hidden" and "pointless" aren't the same thing — a background tab doing the actual relaying for a visible one is exactly what you want, not what you're trying to kill.
The real problem was never "which tabs should connect." It's that six tabs are duplicating work that only needs to happen once. You don't want fewer connections scattered unpredictably across whichever tabs happen to be visible — you want exactly one connection, owned by exactly one tab, with the other five just listening in.
That's leader election, and the browser already ships the primitive for it.
navigator.locks.request() normally looks like a mutex: acquire a named lock, do some work, let it resolve, the lock releases. But nothing requires the callback to resolve quickly. Give it a promise that never resolves on its own, and the lock stays held for as long as that tab is open — the moment the tab closes, crashes, or navigates away, the browser releases the lock for you, automatically, no cleanup code required.
That's the whole mechanism. Whichever tab's request() call wins the (exclusive, FIFO) queue becomes the leader. Every other tab's request just waits — and the instant the leader's tab closes, the next one in line is granted the lock and takes over.
Runs right in your browser — poke at it and watch the concept react live.
Electing a leader only solves half the problem — the other five tabs still need the updates. That's what BroadcastChannel is for: the leader relays every message it receives onto a channel that every other tab is already listening to.
Here's the gotcha worth knowing before you build this: a BroadcastChannel never delivers a message back to the instance that posted it — that's by design, not a bug. So the leader can't just post to the channel and expect its own onmessage to fire; that's why the leader's socket.onmessage above renders directly instead of waiting for an echo that will never arrive. Followers never touch the real connection — they only ever get updates through the channel. If a follower needs to send something back (a user action, a form submit), it posts that onto the channel too, and the leader is the only one that forwards it to the server. One tab talks to your backend. The rest just listen.
This is the part worth testing yourself, because it's the part that makes this worth doing at all: close the leader tab, and one of the waiting tabs is granted the lock as soon as the browser notices that tab's context is gone — no polling, no "is anyone still there" timeout, no stale leader lingering for thirty seconds before a new one takes over. The browser's lock queue is the failover mechanism — you don't write one.
There's a brief gap you should know about: the new leader has to open its own WebSocket connection after acquiring the lock, so there's a reconnect delay (normal connection-establishment time) before live updates resume. For a dashboard or notification feed, that's invisible. For something that can't tolerate a gap at all, keep that in mind rather than assuming the handoff is instant.
One more option worth knowing: navigator.locks.request(name, { ifAvailable: true }, callback) checks leadership without joining the queue — callback runs immediately with null if someone else already holds the lock, instead of waiting. Useful for a tab that wants to know "am I the leader?" without volunteering to become one.
Everything above is easy to agree with and easy to get wrong in practice — the FIFO ordering, the automatic release, the reconnect gap. The playground above runs the exact same navigator.locks + BroadcastChannel code in three simulated tabs on one page. Open each one, watch the lock queue hand off leadership the moment you close the leader, and try the "steal" option to see what happens when a tab forces its way to the front instead of waiting in line.
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
navigator.locks is Baseline widely available — Chrome 69+, Firefox 96+, Safari 15.4+ — so this works in every current browser. The one footgun is the steal option: it forcibly yanks the lock away from whoever holds it, skipping the queue entirely. That's occasionally the right tool (a user force-reloading a stuck tab), but use it knowing the previous leader never gets a chance to finish cleanly — it just loses the lock mid-flight.
You don't need six tabs each holding their own connection to do the job of one. You need one tab doing the job, and a browser API that already knows how to pick which one — and hand the job to the next tab the moment the first one leaves.
Does your app open a connection per tab right now? Go count it in the Network panel — I'll bet it's more than you think.
- Your Timer Doesn't Know You Tabbed Away
- Stop using the localStorage hack to sync browser tabs. BroadcastChannel does it natively.
- Five tabs open, one refresh token — the race nobody noticed
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.