Web Locks API: Synchronize Resources Across Tabs
The Web Locks API is a built-in browser standard that allows JavaScript code running across multiple browsing contexts—such as different tabs, windows, iframes, or Web Workers—to coordinate access to shared resources. This article explains the core concepts of the Web Locks API, compares exclusive and shared lock modes, explores key options like non-blocking requests, and demonstrates practical code implementations to prevent race conditions and manage shared state effectively.
The Multi-Tab Concurrency Problem
Modern web applications often run concurrently across multiple
browser tabs. When these tabs read from and write to shared storage
mechanisms like IndexedDB, localStorage, or Cache Storage
at the same time, race conditions can occur. Traditional JavaScript
execution is single-threaded per context, but across multiple contexts,
operations interleave unpredictably.
Common issues include: - Concurrent read-modify-write operations overwriting data in IndexedDB. - Multiple tabs simultaneously attempting to refresh an expiring authentication token. - Redundant background network requests or duplicate WebSocket connections.
The Web Locks API solves these issues by providing a standardized mutual exclusion mechanism.
How the Web Locks API Works
The API operates under navigator.locks. It allows an
execution context to request a named lock. The lock is held for the
duration of an asynchronous callback function passed to
navigator.locks.request(). Once the returned Promise
resolves or rejects, the browser automatically releases the lock for
waiting contexts.
Basic Exclusive Lock
By default, locks are exclusive. Only one context can hold an exclusive lock with a given name at any time.
async function updateSharedData(newData) {
await navigator.locks.request('database_sync_lock', async (lock) => {
// Critical section: Only one tab executes this block at a time
const currentData = await readFromIndexedDB();
const updatedData = { ...currentData, ...newData };
await writeToIndexedDB(updatedData);
console.log('Data successfully updated by the lock holder.');
});
// The lock is automatically released here
}If Tab A is executing the callback, Tab B will pause at
navigator.locks.request('database_sync_lock', ...) until
Tab A finishes.
Lock Modes: Exclusive vs. Shared
The API supports two distinct modes configured via the
mode option:
exclusive(Default): Grants access to only one requester. All other requests (exclusive or shared) must wait in a FIFO queue.shared: Allows multiple contexts to acquire the lock concurrently, provided no exclusive lock on the same resource is active. This mirrors the reader-writer pattern, making it ideal for read-only tasks that should not run alongside writes.
// Reader (Shared)
async function readData() {
await navigator.locks.request('resource_name', { mode: 'shared' }, async (lock) => {
return await readFromStorage();
});
}
// Writer (Exclusive)
async function writeData(data) {
await navigator.locks.request('resource_name', { mode: 'exclusive' }, async (lock) => {
await writeToStorage(data);
});
}Advanced Request Options
Non-Blocking Requests
(ifAvailable)
By default, lock requests queue until the resource is free. Passing
{ ifAvailable: true } instructs the browser to evaluate the
lock immediately without queueing. If the lock is held elsewhere, the
callback executes with null instead of a lock object.
await navigator.locks.request('leader_election', { ifAvailable: true }, async (lock) => {
if (!lock) {
console.log('Another tab is already the leader.');
return;
}
console.log('This tab is now the designated leader.');
// Keep the lock active to maintain leadership
await keepAlive();
});Abort Signals and Timeouts
Lock requests can be cancelled while waiting in the queue using
standard AbortController signals.
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000); // 3-second timeout
try {
await navigator.locks.request('auth_token_refresh', { signal: controller.signal }, async (lock) => {
await refreshToken();
});
} catch (err) {
if (err.name === 'AbortError') {
console.warn('Failed to acquire lock before the timeout.');
}
}Inspecting Lock State
The API provides navigator.locks.query() to inspect
active and pending locks across the origin. This is useful for
diagnostics, debugging, and metrics.
const state = await navigator.locks.query();
console.log('Held locks:', state.held);
console.log('Pending requests:', state.pending);Primary Use Cases
- Token Refresh Coordination: Ensures only one tab makes a network request to refresh authentication credentials while other tabs wait for the new token.
- Leader Election: Enables a single active tab to handle background polling, WebRTC connections, or WebSocket communication, offloading redundant network traffic.
- Atomic Storage Mutations: Guarantees sequence safety when reading, modifying, and persisting transactional data across offline databases.