Chrome Extension Background Queue Processing — Best Practices

7 min read

Background Queue Processing Patterns

Overview

Chrome extensions often have multiple contexts (popup, content scripts, options page) that submit work to the background service worker. Processing requests immediately can overwhelm the system, cause race conditions, or fail when the service worker is terminated. A queue-based architecture ensures work is processed sequentially, survives restarts, and handles failures gracefully.

Persistent Task Queue

Store queue in chrome.storage.local to survive service worker termination:

// background.ts
const QUEUE_KEY = "taskQueue";
const MAX_RETRIES = 3;

async function enqueueTask(task) {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  task.id = crypto.randomUUID();
  task.status = "pending";
  task.retries = 0;
  task.createdAt = Date.now();
  queue.push(task);
  await chrome.storage.local.set({ [QUEUE_KEY]: queue });
  scheduleProcessing();
}

async function processQueue() {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  const pending = queue.filter(t => t.status === "pending");
  
  for (const task of pending) {
    try {
      task.status = "processing";
      await chrome.storage.local.set({ [QUEUE_KEY]: queue });
      
      await executeTask(task);
      
      task.status = "completed";
      task.completedAt = Date.now();
      await chrome.storage.local.set({ [QUEUE_KEY]: queue });
    } catch (error) {
      await handleFailure(task, error, queue);
    }
  }
}

async function handleFailure(task, error, queue) {
  task.retries++;
  if (task.retries < MAX_RETRIES) {
    task.status = "pending";
    task.lastError = error.message;
  } else {
    task.status = "dead-letter";
    task.failedAt = Date.now();
  }
  await chrome.storage.local.set({ [QUEUE_KEY]: queue });
}

function scheduleProcessing() {
  chrome.alarms.create("processQueue", { delayInMinutes: 0.1 });
}

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === "processQueue") processQueue();
});

Priority Queue

Process urgent items first by sorting before processing:

async function processPriorityQueue() {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  
  // Sort: priority (high first) then by creation time
  queue.sort((a, b) => {
    if (b.priority !== a.priority) return b.priority - a.priority;
    return a.createdAt - b.createdAt;
  });
  
  const pending = queue.filter(t => t.status === "pending").slice(0, 10);
  
  for (const task of pending) {
    await processTask(task, queue);
  }
}

Batch Processing with Alarms

Process N items per wake cycle, schedule alarm for remainder:

const BATCH_SIZE = 5;

async function processBatch() {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  const pending = queue.filter(t => t.status === "pending");
  
  const batch = pending.slice(0, BATCH_SIZE);
  
  for (const task of batch) {
    await processTask(task, queue);
  }
  
  await chrome.storage.local.set({ [QUEUE_KEY]: queue });
  
  // Schedule next batch if more items remain
  const remaining = queue.filter(t => t.status === "pending").length;
  if (remaining > 0) {
    chrome.alarms.create("processBatch", { delayInMinutes: 0.5 });
  }
}

Deduplication

Prevent duplicate entries based on task signature:

async function enqueueWithDedup(task) {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  
  const isDuplicate = queue.some(t => 
    t.status !== "completed" && 
    t.type === task.type && 
    t.signature === task.signature
  );
  
  if (isDuplicate) return;
  
  queue.push({ ...task, id: crypto.randomUUID(), status: "pending" });
  await chrome.storage.local.set({ [QUEUE_KEY]: queue });
}

Queue Monitoring in Popup

Display queue depth and status to users:

// popup.ts - show queue status
async function updateQueueDisplay() {
  const { [QUEUE_KEY]: queue = [] } = await chrome.storage.local.get(QUEUE_KEY);
  
  const pending = queue.filter(t => t.status === "pending").length;
  const processing = queue.filter(t => t.status === "processing").length;
  const completed = queue.filter(t => t.status === "completed").length;
  const failed = queue.filter(t => t.status === "dead-letter").length;
  
  document.getElementById("queue-status").textContent = 
    `Pending: ${pending} | Processing: ${processing} | Done: ${completed} | Failed: ${failed}`;
}

chrome.storage.onChanged.addListener(updateQueueDisplay);

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.