The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Request an exclusive lockawait navigator.locks.request('sync', async lock => runSync())View examples
Detect Web Locksif (!navigator.locks) useFallback()View examples
Return a callback valueconst result = await navigator.locks.request('cache', () => rebuildCache())View examples
Request a shared lockawait navigator.locks.request('catalog', { mode: 'shared' }, readCatalog)View examples
Try without waitingawait navigator.locks.request('leader', { ifAvailable: true }, lock => lock && lead())View examples
Bound lock waitingawait navigator.locks.request('sync', { signal: AbortSignal.timeout(5000) }, synchronize)View examples
Cancel a pending requestcontroller.abort(new DOMException('Navigation', 'AbortError'))View examples
Hold leadershipawait navigator.locks.request('primary', () => leadershipLifetime)View examples
Broadcast stateconst channel = new BroadcastChannel('app-coordination')View examples
Close messagingchannel.close()View examples
Order nested locksfor (const name of [...names].sort()) await acquire(name)View examples
Snapshot lock stateconst { held, pending } = await navigator.locks.query()View examples
Use a storage transactionconst transaction = database.transaction(['queue'], 'readwrite')View examples
Avoid reserved namesconst lockName = `document:${documentId}`View examples

The Web Locks API serializes cooperating same-site contexts that share a storage bucket. A named lock is an application-defined scheduling primitive, not an operating-system lock, server mutex, security boundary, or durable transaction. Keep callbacks short, abort pending requests, use storage-native transactions for data integrity, and design recovery because a tab can disappear at any point.

Step by step

Detailed examples

01

Understand storage-bucket scope and callback lifetime

navigator.locks is exposed to windows and workers in secure contexts. Lock names coordinate agents sharing the same storage bucket in one user agent; they do not cross browsers, profiles, devices, origins, or servers. A lock is held until the callback's returned promise settles, including rejection, then releases automatically. The request promise adopts the callback result. Name resources with stable application identifiers but never place secrets in diagnostic names.

Serialize one local cache rebuild
async function rebuildOnce() {
  if (!navigator.locks) return rebuildCache();
  return navigator.locks.request('cache:v3:rebuild', async () => {
    if (await cacheIsCurrent()) return 'already-current';
    await rebuildCache();
    return 'rebuilt';
  });
}
Back to quick reference ↑
02

Choose exclusive, shared, or conditional acquisition

Exclusive is the default and conflicts with every lock of the same name. Shared mode permits concurrent shared holders but waits behind exclusive work, modeling readers and writers. ifAvailable never waits for conflicting holders and invokes the callback with null; it is still asynchronous. Do not combine ifAvailable with signal or steal. Scheduling is queue-oriented but applications should not turn unspecified timing into correctness assumptions.

Skip duplicate opportunistic maintenance
await navigator.locks.request(
  'maintenance',
  { ifAvailable: true },
  async lock => {
    if (!lock) return false;
    await compactLocalData();
    return true;
  }
);
Back to quick reference ↑
03

Bound waiting and cancel work separately after acquisition

A signal aborts only while a request is pending; once granted it is ignored. If work itself must stop, use a separate application signal inside the callback. AbortSignal.timeout() is convenient but feature-test it or use AbortController with a cleared timer. The steal option preempts holders and can leave their code executing without exclusivity, so reserve it for carefully designed recovery protocols, not ordinary timeouts.

Use separate queue and work cancellation
const queueController = new AbortController();
const timer = setTimeout(() => queueController.abort(), 5000);
try {
  await navigator.locks.request('sync', { signal: queueController.signal }, async () => {
    clearTimeout(timer);
    await synchronize({ signal: workController.signal });
  });
} finally { clearTimeout(timer); }
Back to quick reference ↑
04

Combine ephemeral leadership with explicit messaging

A long-lived unresolved callback can designate one tab as leader, and browser termination releases its lock so a queued contender may take over. Leadership is only local and cooperative; the server must still deduplicate jobs and enforce leases for cross-device work. BroadcastChannel distributes state but messages are neither authenticated across same-origin scripts nor durable, and storage partitioning can limit communication. Close the channel and resolve the leadership promise during orderly shutdown.

Hold leadership until page teardown
const channel = new BroadcastChannel('sync:v1');
let relinquish;
const lifetime = new Promise(resolve => { relinquish = resolve; });
navigator.locks.request('sync:leader', async () => {
  channel.postMessage({ type: 'leader-ready' });
  startLeaderWork();
  await lifetime;
  stopLeaderWork();
});
addEventListener('pagehide', () => { relinquish(); channel.close(); }, { once: true });
Back to quick reference ↑
05

Avoid nested cycles and keep critical sections bounded

Web Locks does not detect deadlock. If tasks need multiple names, acquire them in one documented global order and avoid waiting for code that may need a held lock. Never hold a lock across user prompts, indefinite network operations, or a message round trip without timeout and recovery. Resource names beginning with a hyphen are reserved. Release occurs automatically on callback settlement or agent termination, but abrupt termination can leave external side effects incomplete.

Acquire two resources in a fixed order
async function withDocumentAndIndex(documentId, task) {
  const names = [`document:${documentId}`, 'search-index'].sort();
  return navigator.locks.request(names[0], () =>
    navigator.locks.request(names[1], task)
  );
}
Back to quick reference ↑
06

Use query for diagnostics and storage transactions for correctness

query() returns held and pending arrays with name, mode, and opaque clientId, but the snapshot can be stale immediately. Never check query() and then act as if a lock was acquired; request the lock. A Web Lock coordinates compliant code only and offers no rollback or crash durability. Put IndexedDB reads and writes in one transaction and make external operations idempotent. Treat lock state as potentially sensitive operational telemetry.

Inspect without making a synchronization decision
async function lockDiagnostics() {
  if (!navigator.locks?.query) return null;
  const snapshot = await navigator.locks.query();
  return {
    held: snapshot.held.map(({ name, mode }) => ({ name, mode })),
    pending: snapshot.pending.map(({ name, mode }) => ({ name, mode })),
  };
}
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. World Wide Web ConsortiumWeb Locks APIw3.org
  2. Web Applications Working GroupWeb Locks API: Editors' Draftw3c.github.io
  3. WHATWGHTML Standard: BroadcastChannelhtml.spec.whatwg.org
  4. World Wide Web ConsortiumIndexed Database API 3.0w3.org
  5. WHATWGStorage Standardstorage.spec.whatwg.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback