ScreenshotNeo

BlogScreenshots on your device

Why Chrome’s captureVisibleTab Fails When Vivaldi Works

Diagnose captureVisibleTab failures in Chrome with permissions, activeTab grants, target checks, rate limits, and a working MV3 example.

By the ScreenshotNeo team30 September 20267 min read

Why Chrome's captureVisibleTab Fails When Vivaldi Works

Short answer: Chrome’s tabs.captureVisibleTab() usually fails because the extension lacks the required host permission or a valid temporary activeTab grant, is targeting the wrong window, is trying to capture a restricted URL, or is exceeding Chrome’s documented limit of two calls per second. Vivaldi working does not identify the Chrome cause: Vivaldi says some Chrome extensions behave differently, so compare the manifest, invocation path, URL scheme, browser versions, and exact error text.

This guide gives you a deterministic diagnostic sequence and a minimal Manifest V3 implementation. It also shows when an API such as ScreenshotNeo can remove browser-extension setup from the capture path.

What captureVisibleTab actually captures

captureVisibleTab(windowId?) captures the visible area of the currently active tab in a window. It does not accept an arbitrary tab ID. If windowId is omitted, Chrome uses the current window. The current API returns a Promise containing an image data URL.

That distinction explains a common porting mistake: code that queries a tab, then assumes the tab ID can be passed to captureVisibleTab. Instead, make the intended tab active, or pass its containing window ID and verify that the tab is actually active.

Chrome’s API reference requires either <all_urls> or activeTab. File URLs additionally require the user to allow file access for the extension. Sensitive pages have special restrictions and should be tested against the current Chrome documentation.

Permission and activeTab checks

Start with the manifest. For persistent access to ordinary web pages, request <all_urls> (or narrower host patterns that cover every target). For user-triggered captures, activeTab grants temporary access after an invocation such as clicking the extension action, choosing a context-menu item, using a keyboard shortcut, or accepting an omnibox suggestion.

The capture succeeds only when the permission, active tab, window, and timing checks all line up.
The capture succeeds only when the permission, active tab, window, and timing checks all line up.
{
  "manifest_version": 3,
  "name": "Visible tab capture",
  "version": "1.0.0",
  "permissions": ["activeTab", "tabs"],
  "action": { "default_title": "Capture visible tab" },
  "background": { "service_worker": "background.js" }
}

An activeTab grant is temporary. Navigation to another origin or closing the tab revokes it. Do not assume that installing the extension, opening DevTools, or running arbitrary background code creates the grant. The user must invoke an approved entry point.

Chrome’s documentation also distinguishes restricted pages. Read the method documentation’s special-page rules together with the activeTab guide before promising support for chrome:// pages or other sensitive URLs.

A minimal working Manifest V3 example

This example captures the currently visible tab after the user clicks the extension action and downloads the PNG. Load the directory through chrome://extensions with Developer mode enabled.

// background.js
chrome.action.onClicked.addListener(async (tab) => {
  try {
    if (!tab.windowId) throw new Error("The clicked tab has no windowId");

    const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
      format: "png"
    });

    await chrome.downloads.download({
      url: dataUrl,
      filename: "visible-tab.png",
      saveAs: true
    });
  } catch (error) {
    console.error("captureVisibleTab failed", {
      message: error?.message,
      tabId: tab?.id,
      windowId: tab?.windowId,
      url: tab?.url
    });
  }
});

Add the downloads permission if you use the download call:

{
  "manifest_version": 3,
  "name": "Visible tab capture",
  "version": "1.0.0",
  "permissions": ["activeTab", "downloads"],
  "action": { "default_title": "Capture visible tab" },
  "background": { "service_worker": "background.js" }
}

If you need persistent access instead of a user-invoked capture, replace or supplement activeTab with an appropriate host permission. Keep the host patterns as narrow as your product allows.

Diagnostic sequence

  1. Record the exact error. Open the service worker console from the extension page and copy the complete message. “It works in Vivaldi” is not enough to identify a Chrome failure.
  2. Confirm the active tab and window. Log tab.id, tab.windowId, tab.active, and tab.url. Pass tab.windowId, not tab.id, to captureVisibleTab.
  3. Verify permissions. Check that the manifest contains <all_urls> or activeTab, and that the installed extension has the expected permissions after reloading it.
  4. Verify the invocation path. With activeTab, reproduce the failure by clicking the action, selecting the context-menu command, or using the declared shortcut. A timer or unrelated background event does not establish the grant.
  5. Check the URL scheme. Test an ordinary https:// page first. For file://, enable file access in the extension details and retain the required host permission. Treat chrome:// and other sensitive pages as a separate case.
  6. Check the window. If the user changes windows between the click and the capture, the originally supplied window ID may no longer be the current active window. Query the current window immediately before capture when that race is possible.
  7. Check call timing. Chrome documents a maximum rate of two captureVisibleTab calls per second. Add a queue or backoff instead of firing captures from a fast timer.
  8. Compare environments. Record Chrome and Vivaldi versions, extension version, manifest, target URL, and whether the extension came from the Chrome Web Store or was loaded unpacked.

Common errors and fixes

Symptom Likely cause Fix
Permission denied or cannot access page No <all_urls> host permission and no active activeTab grant Add the appropriate permission, then invoke the extension through a user action and reload the extension.
Works after clicking, fails from a timer activeTab was never granted to the timer’s code path Capture inside the action, context-menu, shortcut, or other documented invocation path; use host permissions for unattended captures.
File URL fails File access is disabled or the required permission is absent Enable “Allow access to file URLs” in the extension details and verify the manifest.
chrome:// or sensitive page fails Restricted-page rules apply Test on a normal web page and follow the API reference’s special-page and activeTab requirements.
Wrong page is captured Assumed a tab ID is accepted, or the active window changed Pass the correct windowId; verify tab.active and query the current window close to capture time.
Intermittent failures during automation More than two calls per second Serialize calls, enforce a minimum 500 ms interval, and back off after a rejected call.
Manifest change has no effect Old extension instance is still running Click Reload on chrome://extensions, then reopen the target tab and invoke the extension again.
Vivaldi succeeds while Chrome fails Browser compatibility difference or different permission state Compare versions, effective permissions, invocation path, target URL, and the exact error before assigning a browser-specific defect.

Rate limiting, performance, and reliability

  • Throttle deliberately: Chrome’s documented ceiling is two calls per second. A single-flight queue prevents bursts from a screenshot loop or multi-tab workflow.
  • Capture only when visible state is ready: wait for your page’s own readiness signal before invoking the API. The method captures what is visible at that instant; it does not wait for network idle or lazy images.
  • Keep diagnostics: log the window ID, URL scheme, invocation source, timestamp, and error message. This separates permission failures from timing failures.
  • Handle tab lifecycle events: a navigation or tab close can revoke activeTab. Re-query the tab and require a new user invocation when necessary.
  • Limit image memory: data URLs can be large for high-resolution pages. Release references after saving and avoid retaining a capture queue in the service worker.

Or skip the browser setup

If your goal is a server-side screenshot rather than an extension-specific capture, ScreenshotNeo’s API documentation provides a single request. It handles consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether the request was billed. Its MCP server also gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools.

A server-side capture can clean common overlays before returning the image.
A server-side capture can clean common overlays before returning the image.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo supports PNG, JPEG, WebP, and PDF output, full-page and element captures, device presets or custom viewports, retina scale, custom CSS and JavaScript, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, caching, signed links, asynchronous jobs, webhooks, bulk capture, and a usage API. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Does captureVisibleTab capture a background tab?

No. It captures the visible area of the currently active tab in the selected window.

Can I pass a tab ID?

No. The method takes an optional window ID. Make the desired tab active and pass its window ID when needed.

Why does activeTab disappear after navigation?

The grant is temporary and can be revoked when the tab navigates to another origin or closes. Invoke the extension again or use an appropriate host permission.

Is Vivaldi proof that Chrome is broken?

No. Vivaldi documents that some Chrome extensions behave differently, but that compatibility note does not identify the cause of an individual failure.

What should I provide when filing a bug?

Include the exact error, manifest, browser versions, target URL and scheme, invocation path, window and tab IDs, and whether the call frequency exceeds two per second.