WebSockets in Chrome Extension Service Workers: Complete Guide (2026)

AppBooster Team · · 4 min read
Network connections and data streams

WebSockets in Chrome Extension Service Workers

WebSockets enable real-time, bidirectional communication between your extension and a server — essential for chat tools, live dashboards, collaborative features, and push-based updates. Since Chrome 116, service workers properly support WebSocket connections with a keepalive mechanism.


The Service Worker Challenge

Chrome terminates idle service workers after approximately 30 seconds of inactivity. Before Chrome 116, this killed any active WebSocket connections. The solution: send periodic keepalive messages to prevent the idle timeout.

Requirements: Chrome 116+ — add this to your manifest:

{
  "manifest_version": 3,
  "minimum_chrome_version": "116",
  "permissions": [],
  "background": {
    "service_worker": "service-worker.js"
  }
}

Basic WebSocket Implementation

let ws = null;
let keepAliveInterval = null;

function connect() {
  ws = new WebSocket('wss://your-server.com/ws');

  ws.onopen = () => {
    console.log('WebSocket connected');
    startKeepAlive();
  };

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    handleMessage(data);
  };

  ws.onclose = (event) => {
    console.log(`WebSocket closed: ${event.code} ${event.reason}`);
    stopKeepAlive();
    ws = null;
    scheduleReconnect();
  };

  ws.onerror = (error) => {
    console.error('WebSocket error:', error);
  };
}

function startKeepAlive() {
  keepAliveInterval = setInterval(() => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
    } else {
      stopKeepAlive();
    }
  }, 20 * 1000); // Every 20 seconds — within the 30s idle timeout
}

function stopKeepAlive() {
  if (keepAliveInterval) {
    clearInterval(keepAliveInterval);
    keepAliveInterval = null;
  }
}

function disconnect() {
  stopKeepAlive();
  if (ws) {
    ws.close(1000, 'Extension disconnecting');
    ws = null;
  }
}

Why 20 seconds? The service worker idle timeout is 30 seconds. Sending a ping every 20 seconds provides a safe margin to keep the worker alive.


Reconnection with Exponential Backoff

Network interruptions happen. Implement automatic reconnection:

let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 10;
const BASE_DELAY = 1000;

function scheduleReconnect() {
  if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
    console.error('Max reconnection attempts reached');
    return;
  }

  const delay = Math.min(BASE_DELAY * Math.pow(2, reconnectAttempts), 60000);
  const jitter = delay * 0.1 * Math.random();

  setTimeout(() => {
    reconnectAttempts++;
    console.log(`Reconnecting (attempt ${reconnectAttempts})...`);
    connect();
  }, delay + jitter);
}

// Reset counter on successful connection
function onConnectionEstablished() {
  reconnectAttempts = 0;
}

Sending and Receiving Messages

Structured Message Protocol

function send(type, payload) {
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    console.warn('WebSocket not connected, queuing message');
    messageQueue.push({ type, payload });
    return;
  }

  ws.send(JSON.stringify({ type, payload, timestamp: Date.now() }));
}

const messageQueue = [];

function flushQueue() {
  while (messageQueue.length > 0 && ws?.readyState === WebSocket.OPEN) {
    const msg = messageQueue.shift();
    ws.send(JSON.stringify(msg));
  }
}

function handleMessage(data) {
  switch (data.type) {
    case 'notification':
      chrome.notifications.create({
        type: 'basic',
        iconUrl: 'icons/icon-128.png',
        title: data.payload.title,
        message: data.payload.body
      });
      break;

    case 'badge-update':
      chrome.action.setBadgeText({ text: String(data.payload.count) });
      break;

    case 'pong':
      // Keepalive acknowledged
      break;

    default:
      // Forward to popup/sidepanel if open
      chrome.runtime.sendMessage(data).catch(() => {});
  }
}

Forwarding Messages to Popup/Side Panel

// service-worker.js
const connectedPorts = new Set();

chrome.runtime.onConnect.addListener((port) => {
  connectedPorts.add(port);
  port.onDisconnect.addListener(() => connectedPorts.delete(port));
});

function broadcastToUI(data) {
  for (const port of connectedPorts) {
    port.postMessage(data);
  }
}

// In handleMessage, replace chrome.runtime.sendMessage with:
// broadcastToUI(data);
// popup.js
const port = chrome.runtime.connect({ name: 'popup' });
port.onMessage.addListener((message) => {
  updateUI(message);
});

Connection State Management

function getConnectionState() {
  if (!ws) return 'disconnected';
  switch (ws.readyState) {
    case WebSocket.CONNECTING: return 'connecting';
    case WebSocket.OPEN: return 'connected';
    case WebSocket.CLOSING: return 'closing';
    case WebSocket.CLOSED: return 'disconnected';
  }
}

// Expose state to popup
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'getStatus') {
    sendResponse({
      state: getConnectionState(),
      reconnectAttempts,
      queuedMessages: messageQueue.length
    });
    return true;
  }
});

When to Use WebSockets vs. Other Options

FeatureWebSocketsWeb Pushchrome.gcm
DirectionBidirectionalServer → ClientServer → Client
Keeps worker aliveYes (with keepalive)No (wakes on message)No (wakes on message)
Battery impactHigherLowerLower
LatencyLowestMediumMedium
Best forLive data, chat, collaborationNotifications, updatesLegacy support

Use WebSockets when you need frequent, bidirectional communication. For infrequent server-to-client updates, prefer Web Push — it’s more battery-friendly and wakes the service worker automatically.


What’s Next

WebSockets give your extension real-time capabilities that set it apart. Implement the keepalive pattern, add reconnection logic, and always provide fallback behavior for offline scenarios.

Grow your real-time extension with ExtensionBooster’s developer tools.

Share this article

Build better extensions with free tools

Icon generator, MV3 converter, review exporter, and more — no signup needed.

Related Articles