Most privacy advice still centers on cookies — clear them, block them, use incognito. Meanwhile, fingerprinting has become the dominant tracking method precisely because it doesn't touch cookies at all. Here's what's actually happening at the API level, and what countermeasures hold up.
The Core Signals (And the Code Behind Them)
Canvas fingerprinting exploits subtle rendering differences between GPU/driver combinations:
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Draw text with specific font, size, and emoji —
// rendering varies by OS font rasterizer and GPU
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('Cwm fjordbank glyphs vext quiz 🎮', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('Cwm fjordbank glyphs vext quiz 🎮', 4, 17);
// Hash the resulting pixel data
return canvas.toDataURL();
}
// Hash the output for compact comparison
async function hashFingerprint(dataUrl) {
const encoder = new TextEncoder();
const data = encoder.encode(dataUrl);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0')).join('');
}
The same code produces different pixel-level output across GPU vendors (NVIDIA vs AMD vs Apple Silicon), driver versions, and even font hinting settings — none of which the user can see, but all of which produce a consistent hash per device.
WebGL fingerprinting goes deeper into hardware identification:
function getWebGLFingerprint() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return null;
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
return {
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL),
// e.g. "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11..."
extensions: gl.getSupportedExtensions(),
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
};
}
This often reveals your exact GPU model, which on its own significantly narrows the population of matching devices.
AudioContext fingerprinting uses hardware-dependent audio processing variance:
async function getAudioFingerprint() {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioCtx.createOscillator();
const analyser = audioCtx.createAnalyser();
const gainNode = audioCtx.createGain();
gainNode.gain.value = 0; // silent — user hears nothing
oscillator.type = 'triangle';
oscillator.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(0);
const buffer = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(buffer);
oscillator.stop();
return buffer.slice(0, 30).join(','); // sample of frequency data
}
Font enumeration via measurement comparison (no direct font list API exists, so it's inferred):
function detectFonts(testFonts) {
const baseFonts = ['monospace', 'sans-serif', 'serif'];
const testString = 'mmmmmmmmmmlli';
const testSize = '72px';
const span = document.createElement('span');
span.style.fontSize = testSize;
span.innerHTML = testString;
document.body.appendChild(span);
const baseWidths = {};
baseFonts.forEach(font => {
span.style.fontFamily = font;
baseWidths[font] = span.offsetWidth;
});
const detected = testFonts.filter(font => {
return baseFonts.some(base => {
span.style.fontFamily = `'${font}', ${base}`;
return span.offsetWidth !== baseWidths[base];
});
});
document.body.removeChild(span);
return detected;
}
Composite Scoring — How These Combine
A single signal rarely identifies anyone uniquely. The entropy comes from combining them:
import math
def calculate_entropy(signal_distribution: dict) -> float:
"""
Shannon entropy in bits — higher = more identifying
e.g. if 1 in 1000 users share your exact value,
that signal contributes ~10 bits of entropy
"""
total = sum(signal_distribution.values())
entropy = 0
for count in signal_distribution.values():
p = count / total
entropy -= p * math.log2(p)
return entropy
# Example combined fingerprint entropy
signals = {
'screen_resolution': 4.2, # bits
'canvas_hash': 8.1,
'webgl_renderer': 6.7,
'font_list': 5.9,
'timezone': 2.1,
'audio_fingerprint': 5.3,
}
total_entropy = sum(signals.values()) # ~32.3 bits
# 2^32.3 ≈ 5.3 billion possible combinations
# Global population ~8 billion — this fingerprint
# alone approaches unique identification
This is why the EFF Panopticlick research consistently found most tested browsers had fingerprints unique among hundreds of thousands of samples — the combinatorial entropy adds up fast even when no individual signal is rare.
What Actually Reduces Entropy (Tested)
Firefox privacy.resistFingerprinting standardizes the highest-entropy signals:
Testing before/after on the same machine with amiunique.org:
| Signal | Default Firefox | resistFingerprinting |
|---|---|---|
| Canvas | Unique hash | Blocked/randomized |
| Timezone | Local (e.g. PST) | UTC |
| Screen resolution | Exact (e.g. 1512x982) | Rounded (1500x950) |
| Fonts detected | 40+ system fonts | ~12 bundled fonts |
| WebGL renderer | Full GPU string | Generic string |
The tradeoff: sites relying on accurate viewport dimensions for layout can render incorrectly, and some canvas-dependent web apps (image editors, games) break entirely.
Brave's fingerprinting protection takes a different approach — randomizing per-session rather than blocking:
Tor Browser standardizes nearly everything to a single shared profile across all users, which is the only approach that achieves near-zero fingerprint uniqueness — at the cost of significant performance and compatibility overhead.
Detection-Side: If You're Building Anti-Fraud Systems
For legitimate use cases (fraud detection, not tracking users for ads), fingerprinting libraries like FingerprintJS provide production-ready implementations:
import FingerprintJS from '@fingerprintjs/fingerprintjs';
const fpPromise = FingerprintJS.load();
(async () => {
const fp = await fpPromise;
const result = await fp.get();
console.log(result.visitorId); // stable identifier
console.log(result.confidence.score); // 0-1 reliability
})();
Worth noting: privacy-focused browsers and extensions specifically target known fingerprinting libraries, so production fraud detection systems increasingly see degraded signal quality from privacy-conscious users — which itself becomes a (weaker) signal.
Practical Takeaway
Cookie-based privacy controls (clearing cookies, incognito mode, cookie blockers) have zero effect on any of the above. If you're building privacy-respecting infrastructure or just hardening your own setup, the signals that matter are canvas, WebGL, audio context, and font enumeration — and the only consistently effective countermeasures are resistFingerprinting-style signal normalization or full standardization (Tor).
Consumer-level explanation without the code: lucas8.com/incognito-mode-browser-fingerprinting
Top comments (0)