Battery API and permissions fingerprinting are quieter than canvas or WebGL noise, but in 2026 they still trip up scrapers that treat browser fingerprinting as a single problem. If your Playwright or Puppeteer setup passes canvas and WebGL checks but keeps hitting soft blocks, this is often why.
Why Battery and Permissions Fingerprinting Survived
The Battery Status API (navigator.getBattery()) was supposed to be dead. Firefox dropped it in 2017. Chrome restricted it to HTTPS. Yet Chromium-based headless browsers still expose a BatteryManager object, and that object’s static, unchanging values are a fingerprint signal on their own.
In a real browser on a laptop, level fluctuates, charging toggles, and dischargingTime counts down. In a headless Chrome instance, you get level: 1, charging: true, chargingTime: 0, dischargingTime: Infinity — every time, on every session. That pattern is trivially detectable. Anti-bot vendors like Kasada and DataDome include it in their feature vectors alongside noisier signals like canvas fingerprinting.
What the Permissions API Leaks
navigator.permissions.query() is the more underappreciated attack surface. Browsers and headless environments handle permission states differently:
- Real Chrome on macOS returns
promptfornotificationsby default - Headless Chrome (no user profile) returns
deniedfor most permissions - Playwright with
--disable-permissionsreturnsdenieduniformly - Some anti-bot checks query
clipboard-read,microphone, andcamerain sequence and look for the pattern of denials
The combination of uniform denied states across multiple permission types is a strong signal. A real user on a fresh profile gets a mix, because OS-level grants and prior browser sessions create variation. This correlates with similar consistency artifacts found in font fingerprinting detection.
What Still Works in 2026
Spoofing Battery API
Intercepting navigator.getBattery() at the CDP level before the page script runs is reliable. The key is injecting realistic, slowly-varying values:
// Playwright addInitScript example
await page.addInitScript(() => {
const mockBattery = {
charging: false,
chargingTime: Infinity,
dischargingTime: 4320, // ~72 minutes
level: 0.61,
addEventListener: () => {},
removeEventListener: () => {}
};
Object.defineProperty(navigator, 'getBattery', {
value: () => Promise.resolve(mockBattery),
writable: false
});
});The important details: charging: false with a realistic dischargingTime (not Infinity) and a level between 0.3 and 0.8. Don’t use round numbers. Rotate the level slightly per session using a seeded random. This pairs well with variance in hardware concurrency and memory spoofing to build a coherent fake profile.
Spoofing Permissions API
Overriding navigator.permissions.query is straightforward but needs to cover the right permission names:
await page.addInitScript(() => {
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => {
if (['notifications', 'clipboard-read', 'microphone'].includes(parameters.name)) {
return Promise.resolve({ state: 'prompt', onchange: null });
}
return originalQuery(parameters);
};
});Return prompt for the commonly-probed permissions, not granted. granted without an actual OS-level grant creates inconsistencies elsewhere. Leave less-probed permissions on the native handler.
Framework and Tool Comparison
Not every scraping stack handles these overrides equally well. Here is the 2026 reality:
| Tool | Battery spoof support | Permissions spoof | CDP injection timing |
|---|---|---|---|
| Playwright | Yes (addInitScript) | Yes (addInitScript) | Before page load |
| Puppeteer | Yes (evaluateOnNewDocument) | Yes | Before page load |
| Selenium + CDP | Partial (requires CDP bridge) | Partial | Timing gaps possible |
| Browserless.io | Config-level patches | Partial | Depends on version |
| Bright Data Scraping Browser | Managed, built-in | Built-in | Managed |
Selenium’s CDP integration is still the weak point. The injection can fire after the page’s first script execution, leaving a detectable window. If you’re on Selenium, move to Playwright or isolate CDP calls to a custom browser extension loaded at startup.
WebGL fingerprinting faces the same timing-gap problem in Selenium, which is why most serious scraping infrastructure has migrated away from it for fingerprint-sensitive targets.
Where These Signals Get Combined
Anti-bot systems don’t rely on Battery API or permissions alone. They aggregate:
- Battery
levelconsistency across sessions (does it change at all?) - Permissions state vs. browser reported features (WebRTC enabled but
microphonedenied?) - Event listener presence on
BatteryManager(real browsers attach them; spoofed ones often don’t) - Timing of
permissions.querycalls (bots query all at once; real users trigger them via UI)
Akamai Bot Manager and PerimeterX (now HUMAN) specifically look at the correlation between multiple static values. A single spoofed signal with everything else default is worse than no spoofing at all, because it creates an inconsistency fingerprint of its own.
The practical numbered checklist before deploying:
- Audit every navigator property your fake profile exposes — use a site like browserleaks.com from inside your headless context
- Ensure battery values vary per session (seed from session ID, not random at runtime)
- Match permissions state to the OS context you’re claiming (macOS Chrome profiles get different defaults than Windows)
- Attach mock event listeners to
BatteryManagereven if they’re no-ops - Test against both Kasada and DataDome challenge pages — they use different weighting
Bottom Line
Battery API and permissions fingerprinting are low-cost signals for anti-bot vendors and high-leverage fixes for scrapers. Patch both at the addInitScript layer, vary values per session, and make sure your overall browser profile is internally consistent before worrying about more exotic signals. DRT covers the full fingerprinting stack if you want to audit every layer your setup exposes.