Prevent Duplicate Workers with Web Locks API

The Web Locks API provides a built-in browser mechanism for coordinating asynchronous tasks across different execution contexts of the same origin. When multiple tabs, iframes, or Web Workers are active simultaneously, background tasks like synchronizing offline data or polling an API can unintentionally run concurrently, causing duplicate operations and data races. By using the Web Locks API, web developers can implement distributed locks to ensure that only one instance of a background worker or process accesses a critical resource or executes a specific job at any given time.

The Problem of Concurrency in the Browser

Modern web applications frequently run tasks in the background using Web Workers, Service Workers, or standard window scripts. If a user opens the same application across five browser tabs, all five contexts might attempt to process the same queue in IndexedDB or upload local changes to a remote server.

Historically, developers addressed this using localStorage flags, SharedWorker, or BroadcastChannel. However, these workarounds are prone to race conditions, fail to handle deadlocks if a tab crashes or is abruptly closed, and add unnecessary architectural complexity.

How the Web Locks API Solves Duplicate Execution

The Web Locks API (navigator.locks) introduces origin-scoped mutexes (mutual exclusions). Any browsing context—whether a tab, an iframe, or a worker—can request a named lock. The browser’s internal lock manager grants the lock to only one requester at a time.

To prevent duplicate job execution, the process follows three primary behaviors:

  1. Named Lock Acquisition: A worker requests a lock using a string identifier representing the task, such as 'sync-offline-data'.
  2. Exclusive Access: By default, locks are exclusive. While one worker holds the lock, any other worker requesting a lock with the same name is placed into a waiting queue.
  3. Automatic Lifecycle Management: The lock is held as long as the callback passed to navigator.locks.request() returns an unresolved Promise. Once the Promise resolves or rejects, the browser automatically releases the lock. If the worker or tab terminates unexpectedly, the lock manager releases the lock immediately, preventing deadlocks.

Implementing Non-Blocking Job Execution

In many background processing scenarios, duplicate jobs should not simply wait in line; they should be skipped entirely if another worker is already handling the task. The Web Locks API supports this via the ifAvailable option:

async function processQueue() {
  await navigator.locks.request(
    'process-queue-lock',
    { ifAvailable: true },
    async (lock) => {
      if (!lock) {
        // Another tab or worker is already running this job.
        console.log('Job is currently being processed elsewhere. Skipping.');
        return;
      }

      // Lock acquired; execute the job.
      await runBackgroundTasks();
    }
  );
}

When { ifAvailable: true } is provided:

  • If no other context holds 'process-queue-lock', the lock is granted, and the callback executes with a valid lock object.
  • If another context is already running the task, the callback executes immediately with null instead of waiting, allowing the secondary worker to safely bail out.

Key Advantages for Background Workers

  • Cross-Context Synchronization: Locks are shared across all tabs, DedicatedWorkers, and SharedWorkers belonging to the same origin.
  • Crash Resilience: Unlike manual storage flags, if a user closes a tab while a background job is running, the lock manager detects the context termination and immediately frees the lock for remaining tabs.
  • Deterministic Sequencing: When queued execution is required instead of skipping, the API processes queued requests in strict first-in, first-out (FIFO) order, preventing race conditions during database writes.