Handling deadlocks in IndexedDB readwrite transactions

Problem Statement

Concurrent readwrite transactions targeting overlapping object stores frequently trigger browser-level deadlock detection. In offline-first architectures, this manifests as:

Before implementing concurrency controls, verify baseline storage constraints and schema design against IndexedDB Architecture & Advanced Patterns. Unoptimized schemas exacerbate lock contention.

Root Cause Analysis

IndexedDB enforces exclusive locks on object stores opened in readwrite mode. When parallel transactions request overlapping stores, the browser queues them sequentially. If a transaction holds a lock beyond engine-specific thresholds (typically ~30s in Chromium, variable in WebKit/Gecko), the runtime automatically aborts pending operations to prevent main-thread blocking.

Common triggers include:

Proper IndexedDB Transaction Management requires strict serialization, minimal lock scope, and deterministic retry logic.

Step-by-Step Implementation Fix

1. Serialize Overlapping Transactions via a Promise Queue

Replace direct transaction instantiation with a serialized queue. This guarantees sequential execution, eliminating race conditions.

// Module-level queue state (use a class or closure in production apps)
let txQueue = Promise.resolve();

/**
 * Enqueues a readwrite operation and returns a promise resolving on commit.
 * Handles AbortError mapping and transaction lifecycle.
 */
export function enqueueWrite(db, storeName, callback) {
 const currentTask = txQueue.then(async () => {
 const tx = db.transaction(storeName, 'readwrite');
 const store = tx.objectStore(storeName);
 
 try {
 await callback(store);
 return new Promise((resolve, reject) => {
 tx.oncomplete = () => resolve();
 tx.onerror = () => reject(tx.error || new Error('Transaction failed'));
 tx.onabort = () => reject(new DOMException('Lock contention detected', 'AbortError'));
 });
 } catch (err) {
 // Fallback for synchronous failures before tx commits
 tx.abort();
 throw err;
 }
 });

 // Advance the queue (swallow queue errors to prevent chain breakage)
 txQueue = currentTask.catch(() => {});
 return currentTask;
}

2. Apply Exponential Backoff Retry for Aborted Transactions

Wrap the queue in a retry mechanism that specifically targets AbortError while surfacing fatal errors (e.g., QuotaExceededError, DataError) immediately.

export async function safeWrite(db, storeName, payload, maxRetries = 3) {
 for (let attempt = 0; attempt < maxRetries; attempt++) {
 try {
 await enqueueWrite(db, storeName, (store) => store.put(payload));
 return; // Success
 } catch (err) {
 // Handle explicit lock contention
 if (err.name === 'AbortError' && attempt < maxRetries - 1) {
 const delay = Math.pow(2, attempt) * 100; // 100ms, 200ms, 400ms
 await new Promise((resolve) => setTimeout(resolve, delay));
 continue;
 }
 
 // Surface fatal errors immediately (Quota, Schema mismatch, etc.)
 if (err.name === 'QuotaExceededError') {
 throw new Error('Storage quota exceeded. Clear cache or prompt user.');
 }
 throw err;
 }
 }
}

3. Minimize Lock Scope

Reduce contention by isolating transactions to single stores and avoiding unnecessary multi-store arrays.

// ❌ High contention risk
db.transaction(['users', 'sessions', 'logs'], 'readwrite');

// ✅ Production-safe: single-store scope
db.transaction('users', 'readwrite');

Validation Protocol

Verify deadlock resolution before shipping to production:

  1. Concurrency Stress Test: Execute 50 concurrent safeWrite() calls targeting the same store. Assert zero AbortError or TransactionInactiveError occurrences.
  2. Transaction State Inspection: Open DevTools > Application > IndexedDB. Monitor the Transactions panel. Verify states transition pendingactivecomplete without aborted flags during sync bursts.
  3. Lock Duration Measurement: Log performance.now() immediately before db.transaction() and inside the tx.oncomplete callback. Assert durations remain under 50ms for standard payloads.
  4. Queue Serialization Assertion: Replace txQueue with a proxy that logs execution order. Confirm strict FIFO resolution under simulated offline/online network toggles.