Web Locks
Why locks?
Section titled “Why locks?”Multiple tabs of the same app can run simultaneously and may attempt to write to the same IndexedDB record or the same OPFS file at exactly the same moment. Without coordination, writes can race and the last writer silently wins (or, worse, data is corrupted). The Web Locks API provides a browser-native mutual-exclusion primitive that solves this — no server required.
navigator.locks.request(name, callback)
Section titled “navigator.locks.request(name, callback)”To acquire a lock, pass a string name and an async callback. The browser holds the lock for as long as the callback’s returned Promise is pending, then releases it automatically.
await navigator.locks.request('demo:my-resource', async (lock) => { // Inside here you hold the lock exclusively. // No other tab can acquire 'demo:my-resource' until this async block resolves. await doWork();});// Lock released here — next waiter can proceed.If the lock is already held by another tab, request queues your callback until the lock is released. The queue is FIFO.
Exclusive vs shared mode
Section titled “Exclusive vs shared mode”The default mode is exclusive — only one holder at a time. For read-heavy workloads you can use shared mode: multiple shared holders can overlap, but an exclusive request waits until all shared holders finish (like a read-write lock).
// Multiple tabs can hold this concurrentlyawait navigator.locks.request('demo:db-read', { mode: 'shared' }, async (lock) => { const data = await db.getAll(); return data;});
// This waits until every shared holder above has finishedawait navigator.locks.request('demo:db-read', { mode: 'exclusive' }, async (lock) => { await db.put({ key: 'x', value: 42 });});ifAvailable — non-blocking try
Section titled “ifAvailable — non-blocking try”Pass ifAvailable: true to get null instead of queuing when the lock is already held. This is useful for “do work only if I can start immediately, otherwise skip this cycle”:
const result = await navigator.locks.request( 'demo:background-sync', { ifAvailable: true }, async (lock) => { if (!lock) { console.log('Lock busy — skipping this cycle'); return null; } // We have the lock return await syncData(); });navigator.locks.query()
Section titled “navigator.locks.query()”query() returns a snapshot of all currently held and waiting locks — useful for debugging:
const state = await navigator.locks.query();console.log('Held:', state.held);console.log('Pending:', state.pending);