4. Multiple Browser Tabs
When a user opens your app in another browser tab, it creates a new WebSocket connection, resulting in a separate socket instance on the server.
⚠️ Why this happens
Section titled “⚠️ Why this happens”Each tab is a new browser context, with:
- A separate WebSocket handshake
- A unique socket ID
✅ Goal
Section titled “✅ Goal”You want to detect or prevent multiple tabs per client, or treat them as a single session.
1. Opt 01: Use a Shared session ID
Section titled “1. Opt 01: Use a Shared session ID”Use a Shared session ID (via cookies or localStorage)
- Generate a user/session ID once.
- Store it in
localStorageor a cookie. - On each connection, the client sends this ID to the server.
- Server can track or reject duplicate connections.
1.1 🧪 Example:
Section titled “1.1 🧪 Example:”Client-side (index.html)
Section titled “Client-side (index.html)”const sessionId = localStorage.getItem('session_id') || crypto.randomUUID();localStorage.setItem('session_id', sessionId);
const socket = io({ auth: { sessionId: sessionId }});Server-side (server.js)
Section titled “Server-side (server.js)”const activeSessions = new Map(); // sessionId → socket
io.use((socket, next) => { const sessionId = socket.handshake.auth.sessionId;
if (!sessionId) { return next(new Error('No session ID')); }
// Reject if this session is already connected if (activeSessions.has(sessionId)) { return next(new Error('Session already active')); }
socket.sessionId = sessionId; next();});
io.on('connection', (socket) => { console.log('Connected:', socket.sessionId); activeSessions.set(socket.sessionId, socket);
socket.on('disconnect', () => { console.log('Disconnected:', socket.sessionId); activeSessions.delete(socket.sessionId); });});👀 What This Does
Section titled “👀 What This Does”- localStorage is shared across tabs in the same browser, so
sessionIdremains the same. - Server blocks duplicate sessions from connecting.
- If a tab is closed, the socket disconnects and the session becomes available again.
2. Opt 02: Multiple Sockets Per User
Section titled “2. Opt 02: Multiple Sockets Per User”Allow multiple sockets per user.
// Use a Set to store multiple sockets per sessionconst userSockets = new Map();
io.on('connection', (socket) => { const sessionId = socket.handshake.auth.sessionId;
if (!userSockets.has(sessionId)) userSockets.set(sessionId, new Set()); userSockets.get(sessionId).add(socket);
socket.on('disconnect', () => { userSockets.get(sessionId).delete(socket); if (userSockets.get(sessionId).size === 0) { userSockets.delete(sessionId); } });});This way you support multiple tabs, but treat them logically as one user.
3. Uniquesness with UUID
Section titled “3. Uniquesness with UUID”No, crypto.randomUUID() does not guarantee absolute global uniqueness, but it generates a UUID v4 which is statistically unique with extremely high probability.
🔍 What is crypto.randomUUID()?
Section titled “🔍 What is crypto.randomUUID()?”- Part of the Web Crypto API
- Generates a UUID v4 (random)
- Format:
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - Uses 122 bits of randomness
✅ Probability of Collision
Section titled “✅ Probability of Collision”- There are 2¹²² possible UUID v4 values.
- You’d have to generate billions of UUIDs per second for thousands of years to have a statistically significant chance of a collision.
📈 For reference:
The chance of generating a duplicate UUID v4 in 1 billion UUIDs is roughly 1 in 1 quintillion (10¹⁸).
🔐 Is It Safe to Use Globally?
Section titled “🔐 Is It Safe to Use Globally?”Yes, in practice:
crypto.randomUUID()is suitable for session IDs, user IDs, client tracking, etc.- Used widely in production systems without collisions
But:
- It’s not cryptographically verifiable (unlike UUID v5 or signed JWTs).
- It’s not guaranteed by specification — just statistically safe.
✅ Use Cases Where It’s Safe
Section titled “✅ Use Cases Where It’s Safe”- Socket session tracking
- Temporary user identifiers
- Client-side IDs
❌ When Not to Use It
Section titled “❌ When Not to Use It”If you need:
- Deterministic uniqueness across systems
- Signed or verifiable identity (e.g., for secure auth tokens)
Use something like:
- UUID v5 (namespace + input → deterministic)
- JWTs
- Server-generated IDs with checks (e.g. database UUIDs with uniqueness constraint)
🧪 Summary
Section titled “🧪 Summary”| Property | crypto.randomUUID() |
|---|---|
| Type | UUID v4 |
| Uniqueness Guarantee | Statistical (not absolute) |
| Random Bits | 122 |
| Safe for Global Use? | ✅ Yes, for nearly all practical uses |
UUID v5 (deterministic based on input), or a hybrid approach can be used for extra safety.
4. Navigating From Pages
Section titled “4. Navigating From Pages”When you navigate from one page to another in a traditional multi-page web app (MPA), the browser unloads the current page, which closes the existing WebSocket connection. So the socket disconnects.
- WebSocket connection lives as long as the page (tab) is open and active.
- Navigating to a new page reloads the whole page, tearing down the previous socket.
- The new page will create a new WebSocket connection (a new socket).
How to handle this?
Section titled “How to handle this?”- Single Page Application (SPA)
- If you use frameworks like React, Vue, Angular, the app stays loaded.
- Navigation is client-side, so WebSocket stays connected.
- Great for maintaining persistent socket connections.
- Reconnect on page load
- On every page load, your client script should create a new socket connection.
- You can handle reconnection logic automatically with Socket.IO’s built-in support.
const socket = io({ reconnectionAttempts: 5, reconnectionDelay: 1000});- Use SharedWorker or Service Worker (advanced)
- You can create a shared WebSocket connection at the browser level using SharedWorker.
- All tabs/pages can communicate with the worker and share the socket.
- This is more complex and not widely used yet.
Summary:
Section titled “Summary:”| Scenario | Effect on WebSocket connection |
|---|---|
| Multi-page navigation | Socket disconnects and reconnects |
| SPA (client-side routing) | Socket stays connected |
| Manual reconnect on load | New socket created after page load |
On page load, it will be a new socket connection, with a new socket ID from the server.
What happens during page navigation?
Section titled “What happens during page navigation?”- The old WebSocket connection closes (because the page unloads).
- The new page creates a new WebSocket connection.
- The server treats this as a completely new socket.
Implications of new socket on page reload/navigation
Section titled “Implications of new socket on page reload/navigation”- The server sees it as a new client connection.
- Any socket-specific state on the server tied to the old socket ID is lost.
- You need to re-establish any session or user context on the new socket.
How to handle this properly?
Section titled “How to handle this properly?”- Use a persistent session ID or token
- Pass a session token or user ID when connecting.
- On connection, server restores context based on that token (e.g., user data, permissions).
- Re-sync client and server state
- After connection, client emits an event like
restore_statewith info about what it needs. - Server responds with current state/data so client can continue smoothly.
Quick example of passing session ID on connection:
Section titled “Quick example of passing session ID on connection:”// Clientconst sessionId = localStorage.getItem('session_id') || generateNewSessionId();localStorage.setItem('session_id', sessionId);
const socket = io({ auth: { sessionId } });
socket.on('connect', () => { socket.emit('restore_state', { sessionId });});// Serverio.use((socket, next) => { const sessionId = socket.handshake.auth.sessionId; if (!sessionId) return next(new Error("No session ID")); socket.sessionId = sessionId; next();});
io.on('connection', (socket) => { console.log("New connection for session:", socket.sessionId);
socket.on('restore_state', (data) => { // Retrieve state for data.sessionId and send back socket.emit('state_data', {/*...*/}); });});Summary
Section titled “Summary”- New page load = new socket connection with a new socket ID.
- To maintain continuity, use a session ID or token shared between tabs/page loads.
- Restore or sync app state after reconnecting.
5. Shared Worker
Section titled “5. Shared Worker”Here’s a concise explanation plus example on how to use a SharedWorker to maintain a single WebSocket connection shared across multiple tabs of the same origin.
Why use a SharedWorker?
Section titled “Why use a SharedWorker?”- WebSocket connection lives in the worker, independent of any page.
- Multiple tabs connect to the same SharedWorker.
- The worker manages one WebSocket and broadcasts messages to all connected tabs.
- Avoids multiple WebSocket connections per user.
Basic architecture
Section titled “Basic architecture”[Tab 1] \[Tab 2] -- connect --> [SharedWorker] <---> [Single WebSocket connection][Tab 3] /Example: SharedWorker managing a single WebSocket
Section titled “Example: SharedWorker managing a single WebSocket”1. shared-worker.js
Section titled “1. shared-worker.js”let connections = [];let socket;
function setupSocket() { socket = new WebSocket('wss://yourserver.example/ws');
socket.onmessage = (event) => { // Broadcast to all connected tabs connections.forEach(port => port.postMessage({ type: 'message', data: event.data })); };
socket.onopen = () => { connections.forEach(port => port.postMessage({ type: 'open' })); };
socket.onclose = () => { connections.forEach(port => port.postMessage({ type: 'close' })); };}
// Initialize WebSocket connection once the first tab connectsonconnect = (e) => { const port = e.ports[0]; connections.push(port);
if (!socket || socket.readyState === WebSocket.CLOSED) { setupSocket(); }
port.onmessage = (event) => { if (socket.readyState === WebSocket.OPEN) { socket.send(event.data); } };
port.start();
port.postMessage({ type: 'connected' });
port.onclose = () => { connections = connections.filter(p => p !== port); if (connections.length === 0 && socket) { socket.close(); } };};2. Client page script
Section titled “2. Client page script”const worker = new SharedWorker('/shared-worker.js');
worker.port.start();
worker.port.onmessage = (event) => { const { type, data } = event.data; if (type === 'message') { console.log('Received from server:', data); } else if (type === 'open') { console.log('WebSocket opened'); } else if (type === 'close') { console.log('WebSocket closed'); }};
// Send message to server through the SharedWorkerfunction sendMessage(msg) { worker.port.postMessage(msg);}6. Tracking Socket with id
Section titled “6. Tracking Socket with id”Each socket has a unique ID (socket.id). You could also store messages or socket references like:
const messageQueue = [];const clientSockets = new Map(); // socket.id -> socket
io.on('connection', (socket) => { clientSockets.set(socket.id, socket);
socket.on('chat message', (msg) => { messageQueue.push({ msg, senderId: socket.id });
setTimeout(() => { for (const [id, s] of clientSockets.entries()) { if (id !== socket.id) { s.emit('chat message', msg); // send to everyone except original sender } } }, 5000); });
socket.on('disconnect', () => { clientSockets.delete(socket.id); });});