Skip to content

The Battery Status API and the privacy disaster that got it deprecated

· 21 min read
Copyright: MIT
A battery glyph rendered as a wordmark over the four BatteryManager property names

A battery is the least personal thing about a laptop. Everyone has one, they all do the same job, and the number on the screen tells you nothing about who is holding the machine. So when the W3C standardised a way for any web page to read that number without asking, the privacy section of the spec said the impact was minimal. Reasonable on its face. What harm is a percentage?

The harm turned out to be that a percentage is not a percentage. It is a high-resolution float that, combined with two timers also exposed by the same interface, names a specific device for a window of seconds at a time, and does so identically across every site that runs the same third-party script. That is enough to re-link a visitor who just cleared their cookies. This post walks through how a feature meant for power-saving web apps became a short-term tracking primitive, the research that demonstrated it, the two scripts caught using it on real sites, and the rare decision by two browser engines to delete a shipped API rather than patch it.

The sections below go in roughly the order the story happened. First the API itself and what it exposes. Then the entropy problem, the precision bug that made it worse on one platform, and the re-identification mechanism. Then the 2015 paper, the in-the-wild measurement that followed, the spec and browser changes, and finally what the API looks like in 2026 and what it tells us about reviewing a feature before it ships.

What the API actually exposes

The Battery Status API is small. A page calls navigator.getBattery(), which returns a promise that resolves to a single BatteryManager object. That object carries four read-only properties and a matching set of events. There is no method that returns serial numbers, no hardware identifier, nothing that looks like a tracking handle. Just status.

The four properties, per the current W3C specification, are charging (a boolean: is the device plugged in), chargingTime (an unrestricted double: seconds until the battery is full, or 0 if already full), dischargingTime (an unrestricted double: seconds until the battery is empty, or Infinity while charging), and level (a double from 0 to 1.0 representing the current charge). Each property has a paired event that fires when it changes: chargingchange, chargingtimechange, dischargingtimechange, and levelchange. A web app can register a listener and get told the moment the user unplugs.

navigator.getBattery() → Promise<BatteryManager> properties (read-only) charging boolean chargingTime double, seconds dischargingTime double, seconds level double, 0.0 – 1.0 events chargingchange chargingtimechange dischargingtimechange levelchange No permission prompt. No user gesture. Any script on the page can read all four. *The full surface of the BatteryManager interface. The accent marks `level`, the property that does most of the damage.*

The intended use cases were sensible. An email client that defers a large background sync when the battery is low. A video site that drops to a lower bitrate to save power. A document editor that switches to aggressive auto-save when it sees the user is about to run out of charge. All of these want to read level and charging, and all of them are legitimate. The W3C Devices and Sensors group standardised the interface, Chrome shipped it in 2014, and Firefox followed.

The detail that mattered, and that nobody flagged at standardisation time, is in the access model. There is no permission prompt. There is no user gesture requirement. The original API was not even limited to secure contexts. Any script the page loads, including third-party advertising and analytics code the site author never inspected, could call getBattery() and read all four values silently. A feature is only as trustworthy as the least trustworthy script on the page, and on the ad-funded web that bar is on the floor.

Why a battery percentage carries entropy

Fingerprinting works by collecting attributes that are stable enough to recognise a device again and varied enough that few other devices share the same combination. The canonical examples are the canvas rendering signature and the WebGL renderer string, each of which alone narrows a device to a small fraction of the population. Battery state looks like the opposite of that. It is not stable, it changes constantly, and at first glance it has very little variety. A charge percentage is one number between 0 and 100.

The first crack in that reasoning is precision. The spec types level as a double, and an implementation that hands the operating system value straight through to JavaScript can expose far more than two decimal digits. Firefox on GNU/Linux did exactly this. It read the battery level as a 64-bit double-precision float and exposed it to the page essentially unrounded. A reader could see a level like 0.9301929625425652 rather than the 0.93 that Windows, macOS, and Android reported. That extra resolution is the difference between roughly a hundred possible values and a number with many significant digits, and the underlying source is what made it possible.

On Linux, Firefox sourced its battery data from UPower, the system power-management daemon. UPower reports energy values in watt-hours with floating-point resolution. Take the current energy reading, divide by the design capacity, and the quotient is a long fractional number. A device with a 60 Wh battery currently holding 55.83 Wh produces a level value that, at full precision, is close to unique among devices with different absolute capacities. The percentage is the same for a phone and a workstation at 93 percent, but the full-precision float behind it is not, because the divisor differs.

Both devices show the same percentage to the user laptop, 60 Wh design energy = 55.83 Wh 55.83 / 60 93% level = 0.9305000000 phone, 14.7 Wh design energy = 13.68 Wh 13.68 / 14.7 93% level = 0.9306122448 *The displayed percentage hides the divisor. At full float precision, the same "93 percent" resolves to different values because the design capacities differ. Numbers shown are illustrative of the mechanism, not measured readings.*

So the leak is twofold. The full-precision level can reveal something close to the battery’s actual capacity, which is a quasi-stable per-device attribute, and worse for old or heavily used batteries whose worn capacity is unusual. And separately, the precision turns a coarse percentage into a value with enough distinct states to be useful even on platforms that report capacity less directly. The 2015 researchers made both points, and noted the risk is higher for old or used batteries with reduced capacities, where the worn capacity itself can act as a near-identifier.

The capacity angle deserves a second look because it is the part that worried the researchers most. A battery’s design capacity is fixed at manufacture, but its full-charge capacity falls over time as the cells degrade. Two laptops of the same model leave the factory with the same nominal Wh rating and, a year later, have drifted apart: one owner charges to 100 percent every night, the other keeps the machine plugged in at a desk, and the chemistry rewards them differently. The full-charge capacity becomes a slowly changing per-device number. If a page can recover something close to it from the precision of the level readings over a session, it has an attribute that barely moves over weeks and that differs between otherwise identical machines. That is exactly the profile of a good fingerprinting input: stable across visits, varied across devices. The percentage hides it; the float behind the percentage does not.

Worth stressing: recovering exact capacity from level alone is not trivial, and the paper framed it as a risk surface rather than a turnkey attack. The cleanest exposure was the unrounded Linux readout, which is the specific thing Firefox fixed. But the general lesson holds whenever an implementation forwards an operating-system float without quantising it. The number of significant digits a browser is willing to hand out is itself a privacy decision, and it had been made without anyone deciding it.

The re-identification mechanism

Capacity leakage is a slow problem. The sharper attack uses the readouts as a short-term identifier, and it does not need the Linux precision bug at all. It needs three values to move together.

Consider level, dischargingTime, and chargingTime read at one instant. On a discharging laptop, level might be 0.57, dischargingTime might be 8,040 seconds, and chargingTime is Infinity. The combination of those numbers is not globally unique, but among the population of devices visiting a given set of sites in the same few seconds, it is distinctive. Olejnik and colleagues argued the joint state of charge level and discharge time has enough distinct values, and changes slowly enough, to work as a short-lived identifier. The key word is short-lived. The values drift, so the identifier expires. But it does not expire instantly.

Here is the mechanism that makes it a tracking primitive rather than a curiosity. The operating system updates these values on a fixed cadence, and the browser re-reads them on its own interval. Between two updates, all three values are constant. If the same third-party script runs on site A and site B, and the user moves from A to B inside that window, the script reads the identical level, dischargingTime, and chargingTime triple on both sites. The readouts are consistent across the two visits because the update intervals and their timing are identical. The script can therefore link the two visits as the same device.

Same third-party script on two sites, one OS update window t0 t0 + 30s battery values constant across this whole interval visit site A visit site B level=0.57 discharge=8040 level=0.57 discharge=8040 identical triple → same device, linked between A and B the user clears cookies / opens private mode the battery triple does not reset, so the link survives *The attack does not beat cookies by reading them. It survives a cookie clear because the battery readout is not cookie state. Values shown are illustrative.*

The reason this is worse than it sounds is the threat model it defeats. Cookie-based tracking is fragile against a user who clears state. Battery readouts are not browser state the user can clear. If a visitor reads an article on site A, clears cookies or switches to private browsing, then opens site B carrying the same embedded tracker, the battery triple is unchanged. The tracker reconnects the two sessions using a value the user has no control over and probably no awareness of. The readout becomes a bridge across exactly the moment when the user thought they had broken the chain.

None of this requires the precision bug. Coarse two-digit level plus the two timers still produces a usable short-window identifier. The Linux precision issue made the per-device capacity leak worse and extended how distinctive a single reading was, but the cross-site linking trick works on any conforming implementation. That is why fixing precision alone was never going to be the whole answer.

It is worth being precise about what the API does not do, because overstating it does no favours. The battery triple is a short-term identifier, not a persistent one. On its own it cannot follow a device for days. Its value to a tracker is as a bridge across a brief gap, or as one more input to a larger fingerprint that already includes the navigator object, screen and device-pixel-ratio entropy, and the rest. Standing alone it is weak. Bolted onto an existing fingerprint, it adds entropy for free and patches the one weakness a good fingerprint has, which is the moment a user resets their visible state.

The 2015 research

The analysis that started all of this is “The leaking battery: A privacy analysis of the HTML5 Battery Status API,” by Lukasz Olejnik, Gunes Acar, Claude Castelluccia, and Claudia Diaz, published in 2015 (Cryptology ePrint Archive report 2015/616, and presented at the DPM/QASA workshop co-located with ESORICS). It is a short paper with an outsized effect, and most of what the preceding sections describe comes directly from it.

The paper made three moves. It showed that Firefox on Linux exposed level at full double precision, sourced from UPower, where other platforms exposed two significant digits. It showed that full precision plus the underlying energy-over-capacity arithmetic could leak something close to the battery’s actual capacity, a quasi-stable per-device value, with the worst exposure on aged batteries whose capacity is unusual. And it argued that level, dischargingTime, and chargingTime together form a short-term identifier that a shared third-party script can use to relink visits across sites and across a privacy reset.

The recommended fix was modest and is the part worth remembering. The authors did not call for the API to be killed. They pointed out that the fingerprintable surface could be cut sharply with no loss of functionality by reducing the precision of the readouts. A power-saving app needs to know the battery is at roughly 15 percent. It does not need eleven decimal places. Round level to a coarse step, quantise the timers, and the capacity leak and most of the short-term entropy vanish while every legitimate use case keeps working. They reported the issue, Mozilla accepted it, and Firefox reduced the precision of its readouts. The W3C specification was updated to recommend against exposing high-precision values.

That should have been the end of it. A researcher finds a leak, the vendor rounds a number, the spec adds a warning, everyone moves on. The reason it was not the end is that someone went and looked at what scripts were actually doing.

Caught in the wild

In 2016, Steven Englehardt and Arvind Narayanan published “Online Tracking: A 1-million-site Measurement and Analysis” (ACM CCS 2016), the largest web-tracking measurement done to that point. They built an instrumented browser, OpenWPM, that monitored access to interfaces likely to be used for fingerprinting, and crawled the top million sites. Among canvas, font, WebRTC, and AudioContext fingerprinting, they specifically went looking for scripts touching the Battery API. They found them.

Two scripts stood out in their manual analysis. The first, served from go.lynxbroker.de/eat_heartbeat.js, read the current charge level and combined it with other identifying features, including a canvas fingerprint and the local IP address obtained through WebRTC. The second, js.ad-score.com/score.min.js, queried all properties of the BatteryManager interface, pulling the charging status, the charge level, and the time remaining to discharge or recharge, and folded those into a device fingerprint alongside other signals. This is the textbook pattern. Battery state was not the fingerprint. It was one more column in a wide table of attributes, present because it added entropy and because it was free to read.

How the in-the-wild scripts used the readouts battery triple canvas hash WebRTC local IP screen / fonts / UA device ID stable cross-site *The battery triple was a side input, not the whole identifier. It mattered because it stayed constant across the cookie-clear gap the rest of the fingerprint could not bridge alone.*

Olejnik later observed that battery readouts had drawn commercial interest of exactly the kind you would expect, with companies looking at whether access to battery levels could be monetised, for instance through pricing or offers that vary with how desperate a user’s charge state is. Whether or not that particular use materialised at scale, the measurement settled the argument that the API was a real tracking surface and not a theoretical one. The scripts were named, the sites were counted, and the technique was no longer hypothetical. Existing privacy tools, the same paper noted, were not effective at catching these newer and more obscure fingerprinting techniques, which meant users had no practical defence.

The removals

What happened next is unusual. Browser engines rarely delete a shipped, standardised, working API. They deprecate slowly, they gate behind permissions, they warn in the console for years. The Battery Status API got something rarer: removal.

Mozilla moved first in a meaningful way. Bug 1313580, filed by Chris Peterson in 2016, was titled “Remove web content access to Battery API” and its stated purpose was to reduce fingerprinting of users by trackers. Mozilla had already reduced precision in earlier work, but this went further and cut web content off from the API entirely, leaving it available only to privileged internal code. The change shipped in Firefox 52 in March 2017. As Olejnik put it at the time, removing a feature from the web platform specifically over privacy was close to unprecedented, and Mozilla framed the calculus plainly: the API was not worth keeping around for web content given what it enabled.

WebKit reached the same conclusion on its own timeline. Bug 164213, “Remove Battery Status API from the tree,” was filed by Brady Eidson on 30 October 2016, with the reason given directly as a privacy, tracking, and fingerprinting risk, citing the research write-up. Apple had implemented the API in WebKit but had never enabled it for web content on its ports, so Safari users were never actually exposed. The removal patch landed as WebKit commit 208300 on 2 November 2016, reviewed by Sam Weinig, pulling the code out of the tree entirely. Safari never shipped the Battery Status API to the web, and after this, the implementation was gone from the engine too.

2014 Chrome ships the API 2015 Olejnik et al. leaking battery Oct 2016 Englehardt & Narayanan; WebKit removal filed Mar 2017 Firefox 52 removes web access From standardisation to removal in under three years *Three years from "minimal privacy impact" in the spec to two engines pulling the feature.*

Chrome, and the Chromium engine under Edge, Opera, and the rest, took a different path. Rather than remove the API, Chromium kept it and tightened the conditions. It restricted access to secure contexts (the deprecation and removal in insecure origins went through blink-dev), brought it under the Permissions Policy with a battery feature, and kept the precision coarse. The API still exists in Chromium today. The two engines that removed it concluded the feature’s value to users did not justify the surface; the engine that kept it concluded the surface could be managed. Both are defensible readings of the same evidence, and the split is the reason the API is in the odd half-alive state it occupies now.

Where the API stands in 2026

The result of those decisions is a clean browser split that has held for years. Chromium-based browsers, which is Chrome, Edge, Opera, Samsung Internet, and the rest, support navigator.getBattery() on desktop and Android, gated to secure contexts and the Permissions Policy. Firefox does not expose it to web content and has not since version 52. Safari does not expose it on macOS or iOS and never did. Caniuse data puts global support somewhere around 79 percent, almost entirely the Chromium share.

The W3C specification is still alive as a Working Draft, with the most recent revision dated 24 October 2024 under the Devices and Sensors Working Group. The interface is unchanged: same BatteryManager, same four properties, same four events, same navigator.getBattery() returning a promise. What changed is the security and privacy text. The spec now states that the user agent should not expose high-precision readouts of battery status information, since that introduces a fingerprinting vector, and it describes additional mitigations: gating behind a secure context, restricting or prompting in private browsing, allowing the user agent to obfuscate values, and emulating a fully charged, plugged-in battery as the default when real data cannot be reported. The exact rounding step a given browser applies is an implementation choice rather than a spec-mandated constant, so the precise quantisation is not fixed by the standard.

For someone building or studying a detector today, the practical reading is straightforward. The presence of navigator.getBattery is itself a coarse engine signal: a Firefox or Safari user agent string paired with a working battery promise is a contradiction worth flagging, the same class of inconsistency that powers a lot of runtime fingerprinting. The values that do come back from Chromium are too coarse and too volatile to anchor a long-lived identity, so the API has gone from a useful free signal to a minor consistency check. It did not become safe so much as it became boring, which from a privacy standpoint is the win.

There is a second, sharper detection angle that has nothing to do with the readings and everything to do with how they are faked. Automation stacks and anti-detect browsers that spoof a Chromium user agent have to decide what getBattery() returns. Get it wrong and the spoof leaks. A headless or virtualised environment running on mains power has no battery at all, and the spec’s own default for that case is the giveaway: an emulated value reporting a fully charged, plugged-in device, charging true, level exactly 1.0, chargingTime 0, dischargingTime Infinity. A real laptop almost never sits at a perfect 1.0 for long. A fleet of crawlers that all report the identical fully-charged tuple is, paradoxically, more distinctive than a fleet that reported nothing, because the uniformity is itself a signal. Spoof the wrong way and the battery that was supposed to be invisible becomes a flag, which is the recurring trap in every spoofing surface: the absence of natural variation is louder than the value itself.

The values that do leak from a genuine Chromium browser are still worth a moment’s accounting. charging is one bit. level is a coarse step. The two timers are volatile and frequently 0 or Infinity. Joined together over a single page load they remain the short-window identifier the 2015 paper described, which is to say useful for linking a few visits in a tight time window and useless for anything longer. That is precisely the residual surface the spec text accepts: enough for a power-saving app to function, not enough to anchor a durable profile, with the understanding that a determined tracker who already has a wide fingerprint can still use the triple as a bridge across a reset.

What the episode is really about

The technical story is small. Four properties, one precision bug on one platform, a short-window identifier, two engine removals. You could fix the immediate problem in an afternoon by rounding a float, and Mozilla did exactly that before deciding the rounding was not enough. The reason the episode keeps getting cited is the process failure underneath it, and that part has not aged.

The Battery Status API was standardised with a privacy section that judged the impact minimal, written by people acting in good faith who simply did not model what a third-party script with no permission prompt would do with a high-resolution float and two timers. Olejnik’s broader point, made repeatedly after this case, was that the W3C process at the time did not require a privacy review at all, and that a feature can look harmless attribute by attribute while being dangerous in combination. A battery percentage is harmless. A battery percentage at eleven decimal places, joined to a discharge timer, readable silently by an ad script, surviving a cookie clear, on a feature with no permission gate, is not. The gap between those two sentences is the whole discipline of privacy engineering, and standards bodies have spent the decade since building the horizontal review steps that would have caught it.

The lasting artefact is not the removal. It is the sentence that now sits in the spec: do not expose high-precision readouts, because that introduces a fingerprinting vector. A future API author reading that line inherits, for free, the lesson that someone else paid for by shipping a tracking primitive to the entire web and then having to claw it back out of two browser engines.


Sources & further reading

Further reading