Browser fingerprinting is one of the most persistent tracking mechanisms on the web - and unlike cookies, you can't just "clear" it. Here's how it actually works under the hood, with real code.
What is browser fingerprinting?
When you visit a website, your browser leaks dozens of attributes: installed fonts, screen resolution, GPU model, audio stack behavior, canvas rendering quirks. Individually these seem harmless. Combined, they form a hash that's statistically unique to your device - a fingerprint.
The creepy part: fingerprinting survives incognito mode, VPNs, and cookie deletion.
1. Canvas fingerprinting
Canvas fingerprinting exploits the fact that different GPUs, drivers, and operating systems render the same drawing instructions slightly differently - different anti-aliasing, sub-pixel rendering, and color profiles produce subtly distinct pixel values.
function getCanvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('Browser fingerprinting test', 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText('Browser fingerprinting test', 4, 17);
return canvas.toDataURL();
}
async function hashString(str) {
const buffer = new TextEncoder().encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
const dataUrl = getCanvasFingerprint();
hashString(dataUrl).then(hash => console.log('Canvas hash:', hash.slice(0, 16)));
Two machines running the same browser version but different GPUs will produce different dataUrl strings - and therefore different hashes.
2. WebGL fingerprinting
WebGL exposes your GPU model directly. WEBGL_debug_renderer_info is a notorious extension that returns the renderer and vendor strings:
function getWebGLFingerprint() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return { renderer: 'none', vendor: 'none' };
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
if (debugInfo) {
return {
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL),
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
};
}
return {
renderer: gl.getParameter(gl.RENDERER),
vendor: gl.getParameter(gl.VENDOR),
};
}
console.log(getWebGLFingerprint());
// Example: { renderer: "ANGLE (NVIDIA GeForce RTX 3080...)", vendor: "Google Inc." }
This alone narrows down your hardware significantly. Combined with the canvas hash, it's highly identifying.
3. AudioContext fingerprinting
Your audio stack introduces tiny floating-point rounding differences when processing audio signals. The AudioContext API exposes this:
async function getAudioFingerprint() {
return new Promise((resolve) => {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return resolve('not_supported');
const context = new AudioCtx();
const oscillator = context.createOscillator();
const analyser = context.createAnalyser();
const gain = context.createGain();
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
gain.gain.value = 0; // silent - we only want the data
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;
oscillator.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(gain);
gain.connect(context.destination);
scriptProcessor.onaudioprocess = (event) => {
const inputData = event.inputBuffer.getChannelData(0);
const fingerprint = inputData
.reduce((acc, val) => acc + Math.abs(val), 0)
.toString();
resolve(fingerprint.slice(0, 20));
oscillator.disconnect();
scriptProcessor.disconnect();
context.close();
};
oscillator.start(0);
});
}
getAudioFingerprint().then(fp => console.log('Audio fingerprint:', fp));
The accumulated float sum differs enough across hardware and OS audio drivers to contribute meaningful entropy to the overall fingerprint.
4. Font enumeration
Browsers don't expose a font list API directly, but you can infer installed fonts by measuring text rendering width with a fallback font:
function detectFont(fontName) {
const baseFonts = ['monospace', 'sans-serif', 'serif'];
const testString = 'mmmmmmmmmmlli';
const testSize = '72px';
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
function measureWidth(font) {
ctx.font = `${testSize} ${font}`;
return ctx.measureText(testString).width;
}
const baseWidths = baseFonts.map(f => measureWidth(f));
return baseFonts.some((base, i) => {
const testWidth = measureWidth(`${fontName},${base}`);
return testWidth !== baseWidths[i];
});
}
const fontsToTest = ['Arial', 'Calibri', 'Futura', 'Gill Sans', 'Comic Sans MS', 'Impact', 'Palatino'];
const detectedFonts = fontsToTest.filter(detectFont);
console.log('Detected fonts:', detectedFonts);
A user with Calibri and Futura installed is already in a much smaller group than average.
Why a VPN does not help
This is the most common misconception. A VPN changes your IP address. That's it.
Your canvas rendering, GPU model, installed fonts, and audio stack are entirely client-side. They have nothing to do with network routing. An attacker fingerprinting you via JavaScript sees the same values whether you're on your home ISP, a VPN endpoint, or Tor with standard browser settings.
To meaningfully resist fingerprinting you need:
-
Browser-level randomization - Firefox's
privacy.resistFingerprinting, Brave's randomized canvas noise - Standardized GPU rendering - Tor Browser forces a fixed window size and disables WebGL
- Font normalization - limiting available fonts to a small baseline set
Seeing it in action
A real fingerprinting library combines 15-30 such signals: screen resolution, timezone, navigator properties, touch support, hardware concurrency, device memory, and more - then hashes them into a single stable ID.
If you're curious about what your own browser is currently leaking, a tool that shows you your own browser fingerprint breaks it down signal by signal - canvas hash, WebGL renderer, audio entropy, font list - and gives you an overall uniqueness score. It's a useful reality check before assuming your incognito tab is actually private.
Reducing your fingerprint surface
| Technique | Effectiveness | Trade-off |
|---|---|---|
| Brave Browser (randomized canvas) | High | Minor rendering artifacts |
| Firefox + resistFingerprinting | High | Some sites break |
| Tor Browser | Very High | Slow, limited usability |
| uBlock Origin | Low (JS blocking only) | Breaks many sites |
| VPN alone | Near zero for fingerprinting | False sense of security |
The web platform keeps adding APIs, and each new one adds potential entropy. The gap between "private browsing" and actual privacy is significant - and understanding the mechanics is the first step to closing it.
Top comments (0)