What Is Browser Fingerprinting and How to Avoid It
Browser fingerprinting IDs your scraper from canvas, WebGL, fonts, and navigator signals, no cookies needed. See what leaks, how to test it, and how to hide.

Browser fingerprinting identifies the exact browser behind a request from dozens of small signals that JavaScript can read: how your GPU draws to a canvas, which WebGL renderer you have, your audio stack, installed fonts, screen size, and navigator properties. None of it needs a cookie, and clearing your cache changes nothing. That is why a scraper can rotate through a thousand fresh IPs and still get flagged as the same bot on every one. This guide covers what gets measured, why headless browsers leak signals a real user never sends, how to read your own fingerprint in code, and the practical ways to present a consistent, real-looking fingerprint that matches your proxy.
Browser Fingerprinting in One Minute
A device fingerprint is a hash built from the values your browser exposes to JavaScript. A tracker or anti-bot script collects a few dozen of them, concatenates the results, and hashes the whole thing into one short ID. Two visitors with the same ID are treated as the same device, even across sessions, incognito windows, and IP changes.
The power of the technique is statistical. Any single signal, like your screen width or your language, is shared by millions of people. Stack twenty of them together and the combination becomes rare. The EFF's original Panopticlick study (2010) found that the majority of browsers it tested were uniquely identifiable from these values alone, and the AmIUnique research by Laperdrix and colleagues (IEEE S&P, 2016) showed that canvas and WebGL rendering are among the highest-entropy signals available. Modern libraries like the open-source FingerprintJS do exactly this in a few hundred milliseconds.
For a scraper, the consequence is blunt: fingerprinting is a per-browser filter, not a per-IP one. Fix your IP reputation and you still lose if every request carries the same tell-tale headless fingerprint.
The Signals That Build Your Fingerprint
Fingerprinting scripts read from four broad buckets: rendering (canvas, WebGL, audio), hardware and display (screen, CPU, memory), the navigator object, and enumerable resources (fonts, plugins, media devices). Here is what each contributes and where automation gives itself away.
| Signal | What it exposes | Relative entropy | How a headless setup leaks |
|---|---|---|---|
| Canvas | Pixel-level text and shape rendering | High | Software renderer produces a canvas hash that clusters across cloud servers |
| WebGL | GPU vendor, renderer, parameters, extensions | High | Returns `SwiftShader` or a generic Mesa renderer with no real GPU |
| AudioContext | Floating-point output of an audio graph | Medium | Consistent value that matches other headless instances on the same image |
| Fonts | Which fonts are installed, by measurement | Medium to high | A bare Linux container ships almost no fonts, which is itself rare |
| Screen | Resolution, `devicePixelRatio`, color depth | Medium | Default 1280x720 with `devicePixelRatio` 1 is a common bot signature |
| navigator | UA, platform, `hardwareConcurrency`, `deviceMemory`, languages | Medium | `navigator.webdriver` is true, `languages` may be empty |
| Plugins / mimeTypes | Installed browser plugins | Low now, but diagnostic | `navigator.plugins.length === 0` on old headless builds |
The insight scrapers miss: it is not any one value that flags you, it is the combination and its internal consistency. A navigator.platform of Win32 next to a WebGL renderer that says Apple M2 is a contradiction no real machine produces, and that contradiction scores against you even if each value looks fine on its own.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Canvas Fingerprinting Explained
Canvas fingerprinting asks your browser to draw text and shapes to an off-screen , then reads the resulting pixels back. The same drawing instructions produce slightly different pixels on different machines because GPU, graphics driver, anti-aliasing, and font rasterization all vary. Hash those pixels and you get a stable, high-entropy identifier that survives cache clears.
Here is the core of what a tracker runs:
function canvasFingerprint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = "16px 'Arial'";
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText('SparkProxy fingerprint ๐', 2, 15);
// The data URL differs by GPU/driver/font stack; hash it into an ID
return canvas.toDataURL();
}
The emoji and the layered fills are deliberate. Emoji rasterization and sub-pixel anti-aliasing are two of the most machine-specific things a browser does, so they widen the entropy. A tracker hashes the toDataURL() string (often with getImageData for the raw bytes) and stores the hash.
The headless tell is subtlety in reverse: a fleet of cloud servers running the same container image, the same driver, and a software renderer will produce the same canvas hash across every instance. Real users almost never collide. A canvas hash shared by 400 visitors in an hour is a strong bot signal on its own.
WebGL Fingerprinting Explained
WebGL fingerprinting works on the same idea as canvas but reaches deeper into the GPU. Two things leak here: the GPU identity strings, and the pixel output of a rendered 3D scene.
The identity strings come from the WEBGL_debug_renderer_info extension. Privacy modes can mask these, but most browsers still return them:
function webglVendorRenderer() {
const gl = document.createElement('canvas').getContext('webgl');
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return {
vendor: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
renderer: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
};
// Real desktop: "Google Inc. (NVIDIA)", "ANGLE (NVIDIA GeForce RTX 3060 ...)"
// Headless cloud VM with no GPU: "Google Inc. (Google)", "Google SwiftShader"
}
That SwiftShader string is one of the loudest headless tells there is. It means Chrome fell back to software rendering because the machine has no real GPU, which is normal in a data center and abnormal on a consumer laptop. Anti-bot vendors keep a list of software-renderer strings for exactly this reason.
Beyond identity, WebGL exposes a long list of numeric parameters (MAX_TEXTURE_SIZE, MAX_VERTEX_ATTRIBS, the sorted set of supported extensions), and a rendered scene can be read back and hashed just like canvas. Together they push WebGL into the highest-entropy tier of fingerprinting signals.
Audio, Fonts, and the Rest
Three more signal families round out a typical fingerprint.
Audio. An OfflineAudioContext runs a sound through an oscillator and a compressor, then reads the output samples. Floating-point math in the audio stack differs just enough between devices to be measurable, and it needs no permission and makes no sound.
async function audioFingerprint() {
const ctx = new OfflineAudioContext(1, 44100, 44100);
const osc = ctx.createOscillator();
osc.type = 'triangle';
osc.frequency.value = 10000;
const comp = ctx.createDynamicsCompressor();
osc.connect(comp);
comp.connect(ctx.destination);
osc.start(0);
const buffer = await ctx.startRendering();
let sum = 0;
for (let i = 4500; i < 5000; i++) {
sum += Math.abs(buffer.getChannelData(0)[i]);
}
return sum.toString(); // stable per device, hash it
}
Fonts. Font enumeration measures the pixel width and height of a test string rendered in each candidate font, comparing against a fallback. If the dimensions change, the font is installed. Your exact font list reflects your OS, your locale, and the apps you have installed, which makes it surprisingly identifying. A stripped Linux scraping container ships almost no fonts, and "this browser has 3 fonts" is itself a rare, suspicious value.
Everything else. screen.width, screen.height, devicePixelRatio, and screen.colorDepth describe your display. Intl.DateTimeFormat().resolvedOptions().timeZone gives your timezone. navigator.hardwareConcurrency and navigator.deviceMemory report CPU cores and RAM buckets. Each is low entropy alone, but every added value makes the combined hash rarer.
Why Headless Browsers Leak
A headless Chrome or a Playwright-driven browser is a real browser engine, so it renders canvas and WebGL correctly. It gets caught on the seams between automation and a genuine user session. The main leaks:
navigator.webdriveris true. The W3C WebDriver spec requires automated browsers to set this flag. A real user's browser reportsfalseorundefined. This is the single most checked automation signal.- No real GPU. As covered above, a cloud VM returns a software WebGL renderer like
SwiftShaderinstead of a consumer GPU string. window.chromeis missing or thin. Real Chrome exposes a populatedwindow.chromeobject. Older headless builds omit it, and stealth patches that fake it often get the shape wrong.- Permission inconsistencies. Querying
navigator.permissions.query({name:'notifications'})and readingNotification.permissioncan return a contradictory pair (promptversusdenied) that a normal browser never produces. - Empty or default
navigator.languages. An empty array, or alanguageslist that does not match theAccept-Languageheader, is a red flag. - Geo and locale mismatch. This is the one that catches even careful setups. If your proxy exits in Germany but your browser timezone is
America/New_Yorkand your language isen-US, the story does not hold together.
Older headless Chrome also put the literal string HeadlessChrome in the User-Agent. That specific tell is easy to override now, but the deeper signals above are harder to hide because they touch the actual rendering stack.
How Anti-Bots and Trackers Use It
Two very different groups fingerprint you, for opposite reasons.
Trackers (ad networks, analytics vendors) use fingerprinting as a cookie replacement. When a user blocks third-party cookies or browses in a private window, a stable device fingerprint lets the tracker re-identify them anyway. This is the privacy problem that antidetect browsers exist to solve.
Anti-bot systems (Cloudflare, DataDome, Akamai, PerimeterX) use fingerprinting to separate humans from automation. They run a fingerprinting script on the page, then score it three ways:
- Known-bot matching. Fingerprints that match default Playwright, Puppeteer, or Selenium profiles get challenged or blocked outright.
- Consistency checks. They cross-reference signals against each other and against the network layer. A
Win32platform with an Apple GPU, or a US IP with an Asian timezone, is a contradiction that scores against you. - Collision grouping. Requests sharing one fingerprint get grouped and rate-limited together, even across rotating IPs. This is why a botnet on 5,000 residential IPs still gets caught if every browser reports the same canvas hash.
The takeaway mirrors the network layers below it: fingerprinting is something you clear once per browser identity, not once per IP. Getting it right unblocks a gate that fresh proxies alone never open. It sits alongside IP reputation and header order in the broader set of block signals that decide whether a request survives.
Detect Your Own Fingerprint Leaks
Before you fix anything, measure what your automation actually exposes. Run this in the page context of your scraper (via page.evaluate in Playwright or Puppeteer) and read the output:
() => ({
webdriver: navigator.webdriver, // true = exposed
plugins: navigator.plugins.length, // 0 = old-headless tell
languages: navigator.languages, // [] = red flag
hasChrome: 'chrome' in window, // false on bare headless
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
platform: navigator.platform,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
Then check the two high-entropy renderers from a driver. Here is the WebGL and webdriver check in Playwright:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://www.sparkproxy.io")
print("webdriver:", page.evaluate("() => navigator.webdriver"))
print("webgl:", page.evaluate("""() => {
const gl = document.createElement('canvas').getContext('webgl');
const ext = gl.getExtension('WEBGL_debug_renderer_info');
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
}"""))
browser.close()
If webdriver prints True and the WebGL renderer prints something like Google SwiftShader, you are exposed on the two loudest signals at once. A patched Chromium and a machine with a real (or well-faked) GPU string are what change those two lines.
How to Avoid Browser Fingerprinting
There is no header you can set to disable fingerprinting. The only thing that works is presenting a fingerprint that is both realistic and internally consistent, and keeping it consistent with your network layer. Four approaches, from most manual to most managed.
Option 1: Use an antidetect browser
Antidetect browsers give each profile a full, coherent, real-device fingerprint (canvas, WebGL, fonts, screen, navigator) and keep it stable across sessions. They are the right tool when you run many long-lived identities, such as multiple social or marketplace accounts, and you want each to look like a distinct real person. Our roundup of the top antidetect browsers compares the main tools. Pair each profile with a sticky residential IP, and the browser and the network tell the same story. This pattern is exactly what account-based work like social media monitoring depends on.
Option 2: Patch Playwright or Puppeteer
For code-driven scraping, stealth patches hide the obvious automation seams. Plugins like puppeteer-extra-plugin-stealth and playwright-stealth, or newer patched builds, override navigator.webdriver, fill in window.chrome, and normalize permissions. The minimal version is an init script:
page = browser.new_page()
page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
window.chrome = window.chrome || { runtime: {} };
""")
Be honest about the limits. This hides webdriver but does nothing for the software WebGL renderer, and consistency-based detectors increasingly flag the stealth patches themselves. It is an arms race, and a patch that passed last quarter can fail after a vendor update.
Option 3: Match the fingerprint to your proxy geo
Whatever fingerprint you present, make it agree with your IP. If your proxy exits in Germany, set the browser timezone to Europe/Berlin, send Accept-Language: de-DE, and use a screen size common in that market. A residential IP in the right country plus a matching browser locale reads as one coherent user. A mismatch between the two is one of the first things a consistency check looks for, and it is entirely self-inflicted.
Browser fingerprinting is only the JavaScript layer. The TLS handshake carries its own fingerprint (JA3/JA4) that is computed before the page even loads, so a perfect canvas fingerprint over a Python TLS stack is still a contradiction. Solve both layers, not one.
Option 4: Let the SparkProxy Scraping API handle the stack
Keeping canvas, WebGL, audio, headers, TLS, and IP geo mutually consistent across thousands of requests, and current as browsers update, is the hard part. The SparkProxy Scraping API renders each target in a real browser with a stealth profile and routes it through a matching-country proxy, so the fingerprint the site sees is a genuine, consistent one you did not have to assemble.
curl -X GET "https://scrape.sparkproxy.io/api/v1?url=https://example.com&render_js=true&stealth=true&country_code=US&premium_proxy=true" \
-H "X-API-Key: sk-xxxxxxxxxxxxxxxx"
import requests
resp = requests.get(
"https://scrape.sparkproxy.io/api/v1",
headers={"X-API-Key": "sk-xxxxxxxxxxxxxxxx"},
params={
"url": "https://example.com",
"render_js": "true", # real browser engine, real canvas/WebGL
"stealth": "true", # anti-detection profile
"country_code": "US", # proxy geo the fingerprint matches
"premium_proxy": "true", # residential exit IP
},
)
print(resp.status_code, resp.text[:500])
render_js=true runs the page in a real browser so canvas and WebGL are genuine, stealth=true layers on the anti-detection profile, and country_code keeps the exit IP aligned with the browser locale. This is the managed API versus self-managed trade-off: you pay per request and give up some control, and in return you stop maintaining stealth patches every time Chrome ships a new build.
Browser vs TLS vs IP Fingerprinting
Scrapers get blocked at three separate layers, and confusing them wastes weeks. Each reads different data at a different moment, so fixing one does nothing for the others.
| Layer | What it reads | When it happens | Example scheme | Typical fix |
|---|---|---|---|---|
| IP fingerprinting | Source IP, ASN, subnet reputation | At the TCP connection | ASN/subnet blocklists | Clean residential or ISP IPs |
| TLS fingerprinting | The TLS ClientHello shape | During the handshake, before the page | JA3 / JA4 | curl_cffi impersonation or a real browser |
| Browser fingerprinting | Canvas, WebGL, audio, fonts, navigator | After the page loads, via JavaScript | FingerprintJS, canvas hashing | Consistent real-browser fingerprint plus matching geo |
The three stack. A request has to look right at all of them at once. A clean residential IP with a Python TLS fingerprint fails at layer two. A perfect JA4 with a SwiftShader WebGL renderer and navigator.webdriver true fails at layer three. The reliable move is to make one coherent identity, real browser, real-looking fingerprint, matching TLS, matching IP geo, rather than patching each layer in isolation and hoping the seams do not show.
Frequently asked questions
FAQ
Browser fingerprinting identifies your specific browser by collecting values it exposes to JavaScript, such as how your GPU draws a canvas, your WebGL renderer, installed fonts, and navigator properties, then hashing them into one ID. Because the combination is rare, a site can recognize you across sessions without any cookie, and clearing your cache does not reset it.
Canvas fingerprinting draws text and shapes to a hidden 2D canvas and hashes the resulting pixels, which vary by GPU, driver, and font rendering. WebGL fingerprinting reaches into the 3D graphics stack: it reads the GPU vendor and renderer strings and can hash a rendered 3D scene. WebGL usually carries more entropy because it exposes the actual GPU identity, and a software renderer like SwiftShader is a strong headless tell.
No. Fingerprinting was designed specifically to work without cookies, so clearing them or opening a private window changes nothing about your canvas hash, WebGL renderer, fonts, or screen values. Incognito mode blocks stored data, not the live signals a fingerprinting script reads at page load.
No. A proxy changes only your source IP, not the canvas, WebGL, audio, or navigator values your browser reports. This is why fingerprinted automation gets grouped and blocked across a whole proxy pool: every rotating IP still carries the same device fingerprint. You have to fix the fingerprint in the browser and then align the proxy geo to it.
They read automation seams. The clearest is navigator.webdriver, which the WebDriver spec sets to true under automation, while real browsers report false. Others include a software WebGL renderer such as SwiftShader on a GPU-less server, a missing or thin window.chrome object, empty navigator.languages, and a timezone or locale that does not match the proxy IP's country.
Present a real, internally consistent fingerprint that matches your network layer. For many long-lived accounts, an antidetect browser with sticky residential IPs works well. For code-driven scraping, a patched Playwright or Puppeteer plus a timezone and locale that match your proxy country is the baseline, and a managed Scraping API with render_js and stealth enabled removes the maintenance of keeping every layer consistent as browsers update.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon โ claim it before it's gone
Related articles

How to Scrape Airbnb Listings and Prices
Learn how to scrape Airbnb listings and prices: extract fields from Airbnb's embedded JSON, handle date-based pricing, map pagination, and anti-bot defenses.

How to Scrape GraphQL APIs
Learn to scrape GraphQL API data: find the /graphql endpoint, read the query and variables in DevTools, then replay your own queries with cursor pagination.

How to Bypass reCAPTCHA When Web Scraping
How to bypass reCAPTCHA when web scraping the ethical way: how v2 and v3 scoring work, how to raise your reCAPTCHA score, and solvers as a last resort.
