Canvas fingerprinting explained — and how privacyscore.dev detects browser defences

Canvas fingerprinting is the most-cited example of "stateless" tracking on the modern web — a technique that re-identifies your browser without setting a single cookie, and that survives private browsing, VPN switches, and IP rotation. This article explains exactly how it works, why every browser is uniquely identifiable through it, and what privacyscore.dev does when it detects that your browser is fingerprintable.

The thirty-second version

A website asks the browser to draw the same picture — say a string of text in a specific font with anti-aliasing turned on — into a hidden HTML5 <canvas>. It then reads the rendered pixels back as a PNG, hashes them, and stores the hash. The hash is different on every machine because of tiny differences in:

  • GPU drivers — different drivers anti-alias edges differently down at the pixel level.
  • Font rendering stacks — Windows ClearType, macOS subpixel, Linux FreeType, and Android Skia all produce subtly different glyphs.
  • Subpixel order — some monitors are RGB, some BGR, some have pentile arrangements; the browser uses this to position glyph edges.
  • System fonts — fallback to a system-installed font produces a different shape than fallback to a different one.

The result is a roughly 32-byte hash that's stable across visits, stable across browser restarts, and unique enough across the population that combined with two or three other signals (User-Agent, screen resolution, language) it pinpoints you. amiunique.org consistently shows that 80–95% of canvas hashes are unique in their dataset.

The actual code

Here's a stripped-down version of what every fingerprinting script on the web is doing:

const canvas = document.createElement('canvas');
canvas.width = 280;
canvas.height = 60;
const ctx = canvas.getContext('2d');

// Draw a colored rectangle with a curve.
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 100, 60);
ctx.fillStyle = '#069';
ctx.font = '15px "Arial"';
ctx.fillText('privacyscore.dev — fingerprint test', 4, 22);

// Force complex glyphs that exercise the font stack.
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.font = '18px "Arial"';
ctx.fillText('privacyscore.dev — fingerprint test', 4, 45);

// Read the pixel data and hash it.
const dataUrl = canvas.toDataURL();
const hash = await sha256(dataUrl);

That hash is the fingerprint. The text strings are deliberately chosen to maximise the number of glyphs that differ across systems, and the colours and transparencies push the rendering pipeline through enough states that even closely-related GPU drivers diverge.

Why your incognito window is no defence

This is the part most users don't understand: cookies and site data have nothing to do with canvas fingerprinting. The fingerprint is computed from your hardware and software stack, not from anything stored on disk. Open a private window, clear all cookies, even reinstall the browser — the fingerprint is identical, because the GPU drivers, the fonts, and the OS subpixel-rendering settings are identical.

The same goes for VPNs. A VPN changes your IP address. It does nothing to canvas. A site that fingerprinted you before you connected to your VPN can re-identify the same browser instantly after you connect — the IP address is one of dozens of signals, and the canvas hash alone usually narrows you down to a handful of candidates.

The privacyscore.dev twist: detecting defences, not just leaks

Most "am I fingerprintable" tools just compute the canvas hash and tell you "your fingerprint is unique." That's a useful answer, but it misses something important: a privacy-respecting browser can defend against canvas fingerprinting by injecting random noise. We need to detect that defence, because a randomised canvas isn't a leak — it's a privacy win.

The way our detector works: we render the same canvas twice, in quick succession, and compare the two hashes.

// resources/js/fingerprint/canvas.js (simplified)
async function canvasFingerprint() {
    const hashA = await renderAndHash();
    const hashB = await renderAndHash();
    if (hashA !== hashB) {
        // Two identical operations produced two different hashes.
        // The browser is randomising — privacy win, not a leak.
        return null;
    }
    return hashA;
}

If your browser is randomising — which Tor Browser does always, Firefox does when you set privacy.resistFingerprinting to true, and Brave does on its strict shield setting — the two hashes will differ. Our scoring engine treats that as a privacy win and skips the canvas deduction entirely.

If the two hashes are identical, your browser is producing a deterministic, stable fingerprint, and we deduct 15 points from your privacy score. That's a heavy penalty, because canvas alone is one of the strongest single signals an adversary can use.

Browser-by-browser status, end of 2026

Tor Browser
Always randomised. Even before TBB shipped its current letterbox/anti-fingerprinting mode in 2018, the project's design assumption was that any deterministic canvas read is a critical bug. The two hashes differ on every read. Result: privacy win.
Firefox with privacy.resistFingerprinting
Same as Tor Browser, because Firefox's RFP and Tor Browser share the same underlying defence code. Set the flag in about:config and your canvas reads return random data on every call. Result: privacy win.
Brave on default shield ("Standard")
"Farbling" — Brave's own term for adding small per-site, per-session randomisation. Two hashes within the same page load are the same, but two visits to the same site return different hashes. Our two-shot detector running within a single page sees identical hashes and deducts. To detect Brave specifically, you'd need to compare hashes across page loads, which is impractical. Result: deducted, even though Brave is in fact defending. This is a known limitation we'll address when we ship the Brave-specific check.
Brave on "Aggressive" shield
Same as Tor — every read returns randomised data. Two hashes differ. Result: privacy win.
LibreWolf
Inherits Firefox RFP, so same as Firefox-with-RFP. Result: privacy win.
Safari (macOS / iOS)
Apple ships some opportunistic anti-fingerprinting in WebKit but does not randomise canvas. Two reads return identical hashes. Result: deducted.
Chrome / Chromium / Edge / Opera
No defence. Canvas hash is stable across reads, across page loads, across sessions, across IP changes. Result: deducted.

"But isn't a randomised canvas also identifying?"

This is a fair pushback and the answer is "in theory, yes; in practice, no". A trivially randomised canvas — say, a canvas where every pixel gets ±2 added to its RGB value with random noise — is detectable as "this browser is randomising" and that itself is a small fingerprintable signal. You're now in the bucket of "privacy-conscious users running a hardened browser", which is smaller than "all users", and a sufficiently determined tracker can use the randomisation pattern itself as a partial signal.

The reason this isn't a real problem in practice: the bucket of privacy-conscious users is still in the tens of millions globally and growing every year. You're not unique inside that bucket — you're one of a crowd. Trackers that build profiles around stable fingerprints don't get a usable signal from "this user is in the privacy crowd"; they need a stable per-user identifier and randomisation breaks that. The cost-benefit favours the user every time.

What you can actually do today

  1. If you only run one privacy change this year, switch to Firefox and set privacy.resistFingerprinting to true in about:config. It will randomise canvas, audio, and WebGL, normalise your User-Agent and screen size, and clamp timing APIs. Your privacyscore.dev score will jump 30+ points.
  2. If you're already on Brave, switch the global shield from "Standard" to "Aggressive" if you can tolerate the breakage on a few sites. The randomisation moves from per-session to per-read, which is what we (and other detectors) recognise as a defence.
  3. If you need maximum privacy for one specific task, use Tor Browser. It's the gold standard for fingerprinting resistance and the design assumption of the entire project is that fingerprinting must fail by default.
  4. Don't bother with browser extensions that claim to "block" canvas fingerprinting. The good ones are duplicating work that browsers already do natively, the bad ones produce trivially-detectable output that increases your fingerprintability ("user has Canvas Fingerprint Defender installed" is itself a rare signal).

The takeaway

Canvas fingerprinting is the canonical example of why we built privacyscore.dev. It's invisible — you can't tell from the URL bar that a site is doing it, you can't tell from the network tab in DevTools either, and it works exactly the same whether you're in a private window or not. The defence has to come from the browser itself, and once it's there it's effective and free. Knowing whether your browser is currently defending is the first step.

If you want to see your own canvas hash and find out whether your browser is randomising, run the privacyscore.dev scanner — it'll show you the hash itself or a "randomisation detected" indicator, and explain exactly why your score is what it is.

FAQ

Is canvas fingerprinting still used in 2026 or is it old news?

Heavily used. Major adtech vendors (LiveRamp, ID5, FingerprintJS Pro, Iovation, BioCatch and dozens of others) include it in their default fingerprinting suites. The technique is twelve years old at this point and still effective because no major commercial browser has shipped a default-on defence — they're either off (Chrome, Edge, Safari) or opt-in (Firefox, Brave Standard).

Does WebGL fingerprinting work the same way?

Yes, and worse. WebGL exposes the GPU vendor and renderer string ("NVIDIA GeForce RTX 4090") plus all the same rendering quirks as canvas, with even more variation between cards. Firefox RFP returns generic strings ("Mozilla, Mozilla") to defeat this; we detect those and treat them as a privacy win.

Can I just install a Chrome extension that "blocks" canvas?

No, you really can't. The good extensions don't work because Chrome doesn't expose the right hooks for an extension to intercept canvas reads at the level needed; the bad extensions produce trivially-detectable output that itself becomes a fingerprint. The only effective fix is a browser that defends natively.

Why does privacyscore.dev penalise some browsers that are defending?

Brave Standard mode is the main case. Brave farbles canvas per-session, not per-read, so two reads inside the same page give the same hash and our two-shot test concludes the browser isn't randomising. We're aware of this and the next iteration of the detector will add a per-page-load comparison for Brave specifically.

What's the difference between a unique fingerprint and a tracking cookie?

A cookie is data the browser stores on your behalf and sends back on each visit; you can delete it, you can refuse it, the browser shows you what's stored. A fingerprint is computed live from your browser's behaviour and never stored locally — there's nothing for you to delete, the browser doesn't show it to you, and there's no setting that turns it "off" on most browsers. The cookie law doesn't apply to fingerprints because no data is being stored on the user's machine.