The Battery Status API and the privacy disaster that got it deprecated
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.
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.
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.
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.
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.
*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
- Olejnik, Acar, Castelluccia, Diaz (2015), The leaking battery: A privacy analysis of the HTML5 Battery Status API — the original paper showing capacity leakage via Firefox/Linux precision and the level-plus-timers short-term identifier.
- Englehardt, Narayanan (2016), Online Tracking: A 1-million-site Measurement and Analysis — CCS 2016 measurement that caught two scripts reading the Battery API in the wild and names them.
- W3C Devices and Sensors WG (2024), Battery Status API (Working Draft) — the current spec, interface definition, and the security/privacy text recommending against high-precision readouts.
- Mozilla (2016), Bug 1313580: Remove web content access to Battery API — the Firefox bug that pulled the API from web content, shipped in Firefox 52.
- WebKit (2016), Bug 164213: Remove Battery Status API from the tree — the WebKit removal, filed by Brady Eidson, landed as commit 208300.
- Olejnik (2017), Battery Status Not Included: Assessing Privacy in W3C Web Standards — the author’s retrospective on the removals and the W3C privacy-review gap.
- Olejnik (2015), Privacy analysis of the Battery Status API — the researcher’s own landing page summarising the work, the fix, and the in-the-wild follow-up.
- Cimpanu, BleepingComputer (2016), How Battery Status Readouts Can Threaten User Privacy — contemporaneous reporting on the re-identification scenario and vendor reconsideration.
- Lomas, TechCrunch (2015), Battery Attributes Can Be Used To Track Web Users — the no-permission access model and the Firefox-on-Linux full-precision example value.
- caniuse (2026), Battery Status API support table — current browser support, the Chromium-only situation, and approximate global coverage.
Further reading
Encrypted Client Hello (ECH): what it hides and what it doesn't
How ECH encrypts the inner ClientHello, including SNI, with an HPKE key fetched from DNS, what the outer ClientHello still leaks, and where deployment actually stands now that RFC 9849 has shipped.
·20 min readCanvas fingerprinting: how a single toDataURL call identifies a device
Traces how rendering text and shapes to an HTML5 canvas and hashing the toDataURL output yields a stable per-device value, the GPU, driver and font causes behind the variation, the 2012 origin, and how much entropy it really carries.
·22 min readWebGL fingerprinting: the renderer string, precision, and shader quirks
A primary-source reference on WebGL fingerprinting: the UNMASKED_RENDERER and UNMASKED_VENDOR strings, supported extensions, shader precision formats, rendered-image hashing, and the browser mitigations that bucket or hide them.
·24 min read