Browser fingerprinting is the practice of identifying a browser from the characteristics it exposes, such as its TLS handshake, headers, screen, fonts, GPU and network addresses, instead of from something stored on the device. It is why a new proxy IP alone rarely makes a session look new, and why one mismatch, such as a WebRTC leak of your real IP, can undo a careful setup. This guide covers each fingerprint layer, canvas and WebGL fingerprinting, WebRTC leaks and how to test for them, and exactly which signals a proxy changes and which it cannot touch.
The short version
A proxy changes where your traffic comes from: the exit IP, its ASN and its location. It does not change your browser. The TLS handshake, HTTP/2 settings, headers, JavaScript APIs (navigator, screen, timezone, fonts, canvas, WebGL, audio), WebRTC and behavior pass through untouched. Detection cares less about uniqueness than about whether your signals agree, so aim for consistency: timezone, locale and geolocation that match the exit IP, a real browser engine instead of a spoofed User-Agent, and WebRTC that cannot bypass the proxy.
Scope. This explainer is for teams running browsers through proxies for legitimate work: localized QA, ad verification, privacy hardening, or public-data collection within a site’s terms and rate limits. It explains how detection works so you avoid accidental contradictions, not how to get around security on systems you are not authorized to use.
Key takeaways
- A fingerprint is computed, not stored. Clearing cookies or opening a private window does not change your GPU, fonts or TLS stack.
- Fingerprints span the network, TLS, HTTP, JavaScript, WebRTC and behavior layers. A proxy only replaces the network layer.
- Contradictions get flagged more often than uniqueness does: a German IP with a Los Angeles timezone, a Windows User-Agent with a Linux TCP stack, a desktop GPU string on a “phone”.
- WebRTC may expose your real public IP if UDP bypasses an HTTP proxy. This depends on browser and network settings. Test for leaks, then restrict non-proxied UDP when necessary.
- In Playwright, set
locale,timezone_idandgeolocationfrom the exit IP of a sticky session, and use the HTTP proxy URL. - Test in the exact configuration you run in production. Results from different test pages are not comparable scores.
What Is Browser Fingerprinting?
Identification without storageA browser fingerprint is a set of attributes that a website can observe about your browser and device, combined into something stable across visits and distinctive across users. Your screen size and timezone are each shared by millions; twenty or thirty such attributes together can be rare.
Some attributes are read with JavaScript (fonts, the GPU renderer, how your machine draws text). Others are observed passively on the server: cipher-suite order in the TLS handshake, HTTP header order, TCP options.
Banks and payment providers use fingerprints for fraud prevention and to flag logins from unfamiliar devices. Anti-bot vendors use them to separate automated traffic from people. Ad-tech companies have used them to track users without cookies, which is the use regulators object to.
How fingerprinting differs from cookies
The core difference is where the identifier lives. A cookie is an identifier the site stores in your browser and reads back later. A fingerprint is recomputed from your browser’s characteristics on every visit, so there is nothing to delete.
| Aspect | Cookies | Fingerprint |
|---|---|---|
| Where it lives | Stored on your device | Derived from your device, held by the site |
| Can you delete it? | Yes, clear storage | No, only change the inputs |
| Private window | Starts empty | Mostly the same values |
| Visibility to you | Listed in DevTools | Largely invisible |
| Stability | Until expiry or deletion | Until software, hardware or settings change |
Sites use both: the cookie ties visits together cheaply, and the fingerprint survives when the cookie is gone.
The Signal Layers of a Browser Fingerprint
From the TCP packet to your mouseThink of a fingerprint as a stack of layers, ordered by how early in the connection each becomes visible. The last column is the question proxy users care about.
| Layer | Example signals | How it is read | Changed by a proxy? |
|---|---|---|---|
| Network | IP address, ASN, IP type (residential, mobile, datacenter), GeoIP location | Server side | Yes, you get the exit’s |
| TCP/IP stack | TTL, window size, MSS, TCP option order | Server side, passive | Yes, but to the exit’s stack |
| TLS handshake | Cipher suites, extensions, ALPN, curves (JA3, JA4) | Server side, passive | No |
| HTTP/2 | SETTINGS values, window update, pseudo-header order | Server side, passive | No |
| HTTP headers | User-Agent, Accept-Language, header order, Sec-CH-UA-* Client Hints | Server side | No (over HTTPS) |
| navigator and screen | Platform, CPU cores, memory, touch points, resolution, pixel ratio, webdriver | JavaScript | No |
| Timezone and Intl | IANA timezone, UTC offset, date and number formatting | JavaScript | No |
| Fonts | Installed fonts measured by text width or document.fonts | JavaScript, CSS | No |
| Canvas, WebGL, audio | Rendered pixel hashes, GPU vendor and renderer, audio output | JavaScript | No |
| WebRTC | ICE candidates: local and public IP addresses | JavaScript plus STUN | No, and it can expose the real IP |
| Behavior | Mouse paths, scrolling, typing rhythm, navigation timing | JavaScript events | No |
Network and TCP/IP
The IP is the only part of the fingerprint the proxy owns. Sites look up its ASN (consumer ISP, mobile carrier or hosting company), its reputation and its GeoIP location; see datacenter vs residential proxies for how exit types compare.
A subtle point: the TCP connection that reaches the website is opened by the proxy exit, so passive TCP/IP fingerprinting (TTL, window size, MSS, option order) describes the exit’s operating system. Some systems compare that guess with the OS your User-Agent claims, and timing gaps between the TCP round trip and in-page measurements can hint that a relay is in the path.
TLS and HTTP/2
Before any HTTP request, your client sends a TLS ClientHello listing protocol versions, cipher suites, extensions and supported groups. Each browser build produces a characteristic ClientHello, and hashes such as JA3 and the newer JA4 family condense it into a short string. HTTP/2 adds SETTINGS values, the initial window update and pseudo-header order, which differ between engines and libraries.
So a Python client sending a Chrome User-Agent is easy to spot: the headers say Chrome, the handshake says Python. A proxy cannot fix this. With an HTTP CONNECT tunnel or SOCKS5, TLS runs end to end between your client and the website, and the proxy only forwards encrypted bytes.
HTTP headers and Client Hints
Browsers send a predictable set of headers in a predictable order. Chromium-based browsers also send low-entropy Client Hints on HTTPS by default (Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform), and sites can request high-entropy ones through Accept-CH or navigator.userAgentData.getHighEntropyValues(). Override only the User-Agent string and the hints can tell a different story; see MDN’s Client Hints guide.
JavaScript APIs
JavaScript exposes the largest surface: navigator properties (platform, hardwareConcurrency, deviceMemory, maxTouchPoints, languages, webdriver), screen and devicePixelRatio, the timezone from Intl.DateTimeFormat().resolvedOptions().timeZone, fonts, and the rendering surfaces below. Automation adds tells too: navigator.webdriver is true under Playwright, Puppeteer or Selenium, as covered in Headless Browsers 101.
Behavior
The last layer is how the session acts: pointer movement, scrolling, keystroke timing, page order, and whether assets load the way a real page load would. No network tool influences it.
Entropy and Consistency: What Actually Gets Flagged
Contradictions matter more than rarityFingerprinting research measures distinctiveness in bits of entropy. If one in 16 visitors shares your timezone, that attribute carries 4 bits (log2 16). Independent attributes add up, and about 33 bits single out one person among eight billion. Real attributes are correlated, so totals are lower, but the arithmetic shows why a handful of ordinary values can become unique.
How unique browsers really are depends heavily on the population measured. The EFF’s 2010 Panopticlick study found that 83.6% of browsers in its sample had a unique fingerprint. Later large-scale research on a general-audience website found far lower uniqueness, roughly a third of fingerprints, largely because mobile devices and standardized browser builds look alike.
For anti-bot and fraud systems the stronger signal is consistency: do all layers describe one plausible device in one plausible place? Real users are messy but coherent. Automation and clumsy spoofing produce contradictions:
- The exit IP geolocates to Germany, but
IntlreportsAmerica/Los_AngelesandAccept-Languageisen-USonly. - The User-Agent claims Windows, Client Hints say Linux, and the passive TCP fingerprint looks like Linux too.
- The User-Agent claims an iPhone, but
maxTouchPointsis 0 and WebGL reports a desktop GPU. - The WebGL renderer is a software rasterizer such as SwiftShader, which is common in headless servers and rare on consumer laptops.
- A canvas hash changes on every page load, which tells a script the readout is being randomized.
- A residential exit IP in Spain, with a WebRTC candidate revealing a datacenter IP in Virginia.
A unique fingerprint is normal. A fingerprint that contradicts itself is what gets flagged.
Vendors typically combine such checks with IP reputation and behavior into a risk score rather than blocking on one. Our explainer on how DataDome detects automated traffic walks through one vendor’s layered approach.
Canvas, WebGL and Audio Fingerprinting
Same instructions, slightly different pixelsCanvas fingerprinting, first described in the 2012 academic paper “Pixel Perfect”, asks the browser to draw text and shapes into an off-screen <canvas> and then reads the pixels back. The instructions are identical for everyone, but fonts, anti-aliasing, emoji sets, the graphics library, GPU and driver leave small differences. Hash the pixels and you get a value that is stable on one machine and differs across many.
The illustrative snippet below shows what test pages measure; it sends nothing anywhere.
// Illustrative only: how a canvas fingerprint is derived.
async function canvasHash() {
const canvas = document.createElement("canvas");
canvas.width = 280;
canvas.height = 60;
const ctx = canvas.getContext("2d");
ctx.textBaseline = "top";
ctx.font = "16px Arial";
ctx.fillStyle = "#f60";
ctx.fillRect(120, 1, 60, 20);
ctx.fillStyle = "#069";
ctx.fillText("Fingerprint demo 1.0 \u{1F600}", 2, 15);
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
ctx.fillText("Fingerprint demo 1.0 \u{1F600}", 4, 17);
// Tiny rendering differences (fonts, anti-aliasing, GPU, OS)
// change the pixels, so they change the hash.
const bytes = new TextEncoder().encode(canvas.toDataURL());
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
WebGL: the GPU in plain text
WebGL adds a rendered image, hashed like a canvas, plus metadata: extensions, limits such as maximum texture size and, through WEBGL_debug_renderer_info, the unmasked GPU vendor and renderer. The standard VENDOR and RENDERER values are usually generic; the unmasked ones show the hardware. Some browsers return a coarsened renderer string to limit this, as noted on MDN’s WEBGL_debug_renderer_info page.
function webglInfo() {
const gl = document.createElement("canvas").getContext("webgl");
if (!gl) {
return null; // WebGL disabled is itself a signal
}
const info = {
vendor: gl.getParameter(gl.VENDOR), // usually generic
renderer: gl.getParameter(gl.RENDERER), // usually generic
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
extensions: gl.getSupportedExtensions().length,
};
const dbg = gl.getExtension("WEBGL_debug_renderer_info");
if (dbg) {
info.unmaskedVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
info.unmaskedRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
}
return info;
}
When we ran it in Playwright’s default headless Chromium, the generic values came back as WebKit and WebKit WebGL, while the unmasked renderer named SwiftShader, Chromium’s software renderer, even though the test machine had a GPU. That string is rare on consumer devices, and no exit IP, in any country, changes it.
AudioContext
Audio fingerprinting renders a short signal offline through an oscillator and compressor and sums the samples. Floating-point differences in the audio stack make the result vary between engines and platforms.
async function audioSample() {
const ctx = new OfflineAudioContext(1, 5000, 44100);
const osc = ctx.createOscillator();
osc.type = "triangle";
osc.frequency.value = 10000;
const compressor = ctx.createDynamicsCompressor();
osc.connect(compressor);
compressor.connect(ctx.destination);
osc.start(0);
const buffer = await ctx.startRendering();
const samples = buffer.getChannelData(0);
let sum = 0;
for (let i = 4500; i < samples.length; i++) {
sum += Math.abs(samples[i]);
}
return sum; // differs slightly across engines and platforms
}
How browsers push back
Browsers defend in two opposite ways. Brave randomizes canvas, WebGL and audio output per site and session, so the hash stops being stable. Tor Browser and Firefox’s privacy.resistFingerprinting aim for uniformity, so many users report the same values. Randomization can be detected, though: CreepJS, for example, flags values that contradict each other or change between reads.
WebRTC Leaks: What Leaks and Why
UDP does not go through your HTTP proxyWebRTC lets browsers make peer-to-peer audio, video and data connections. To find a working path between two peers, the browser gathers ICE candidates, which are network addresses at which it might be reachable:
- host candidates: the addresses of your local network interfaces, such as
192.168.1.20. - srflx (server reflexive) candidates: your public IP and port as seen by a STUN server on the internet.
- relay candidates: addresses on a TURN relay server, if one is configured.
Any page can create an RTCPeerConnection, point it at a public STUN server and read the candidates, with no permission prompt. This is what a WebRTC leak test does:
// What a WebRTC leak test does under the hood.
async function listIceCandidates() {
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});
pc.createDataChannel("probe"); // forces ICE gathering
const candidates = [];
pc.onicecandidate = (event) => {
if (event.candidate && event.candidate.candidate) {
// e.g. "candidate:... udp ... 203.0.113.7 54321 typ srflx ..."
candidates.push(event.candidate.candidate);
}
};
await pc.setLocalDescription(await pc.createOffer());
await new Promise((resolve) => setTimeout(resolve, 3000));
pc.close();
return candidates;
}
The leak happens because the browser’s proxy setting covers HTTP and HTTPS, while STUN is UDP, which an HTTP proxy does not carry. The STUN request leaves your network directly, the STUN server sees your real public IP, and the page reads it from the srflx candidate. The site now has two IPs for one session.
HTTPS via CONNECT tunnel
Browser ------------------------------> Proxy gateway --> exit IP --> website
| (sees exit IP)
|
| UDP STUN binding request (not proxied)
+----------------------------------------------------> STUN server
|
srflx candidate = your real public IP <---------+
page JavaScript reads it and reports it
What about local IPs?
Older browsers exposed private LAN addresses in host candidates. Current Chrome and Firefox usually replace them with random mDNS names ending in .local, so the srflx public IP is the part that still matters. Check IPv6 separately: if your machine has a public IPv6 address and the proxy path is IPv4, an IPv6 candidate can identify your connection on its own.
A system-wide VPN routes UDP too, so the STUN server usually sees the VPN’s address unless IPv6 or split tunneling bypasses it. See residential proxy vs VPN.
How to Run a WebRTC Leak Test
Five minutes, in the configuration you actually use- Note your real public IP with the proxy off, for example by opening
https://api.ipify.org. - Configure the proxy exactly as you will use it: same browser, same profile, same extension or automation launch flags.
- Open a WebRTC leak test page such as browserleaks.com/webrtc and wait for ICE gathering to finish.
- Compare. The page’s “remote IP” should be the proxy exit. No public IP in the WebRTC candidate list should be your real IPv4 or IPv6 address.
- Repeat after browser updates, extension changes or new automation flags.
Reading the results: a .local host candidate is normal. No candidates means WebRTC is disabled or restricted, which protects the IP but is a less common configuration. A srflx candidate matching your real IP is the leak.
Preventing WebRTC Leaks
Restrict it rather than switch it offChromium exposes one setting, WebRTC IP handling, through several doors, with modes from RFC 8828. The value that stops the leak is disable_non_proxied_udp: TCP on the public interface, UDP only if the proxy supports it. In our Playwright Chromium tests, default_public_interface_only still produced a srflx candidate with the machine’s public IP, so it is not enough.
| Method | Where | What to set | Trade-off |
|---|---|---|---|
| Enterprise policy | Chrome, ChromeOS (91+) | WebRtcIPHandling = disable_non_proxied_udp | Managed devices, fleet-wide |
| Command-line switch | Chromium, automation | --force-webrtc-ip-handling-policy=disable_non_proxied_udp | Per launch; easy in Playwright |
| Extension API | Chrome extensions | chrome.privacy.network.webRTCIPHandlingPolicy | Depends on the extension |
| Firefox prefs | about:config | media.peerconnection.ice.proxy_only_if_behind_proxy, or media.peerconnection.enabled = false | Disabling breaks calls |
| Anti-detect browser | Per profile | The profile’s WebRTC mode | Check the vendor’s options |
The policy is documented in the Chrome Enterprise policy list, and the extension setting in the chrome.privacy API reference. Firefox’s prefs media.peerconnection.ice.default_address_only and media.peerconnection.ice.no_host further limit which interfaces and host candidates are exposed.
Disabling WebRTC entirely ends the leak but breaks video calls and puts you in a small group of browsers with no RTCPeerConnection. Restricting UDP to the proxy removes the real IP while keeping WebRTC usable on most sites.
The ProxyEmpire Proxy Manager extension
For everyday browsing in Chrome, the free ProxyEmpire Proxy Manager extension handles the common mismatches in one place: it includes WebRTC leak protection, matches the browser’s timezone and language to the proxy location, and has a kill switch that stops traffic if the proxy drops. Run a leak test after enabling it, as you would with any extension.
Anti-detect browsers
Anti-detect browsers manage the whole fingerprint as a profile (WebRTC mode, timezone, locale, geolocation, screen, fonts) bound to one proxy, which is practical when you run many separate browser identities, such as agency ad accounts. ProxyEmpire has setup guides for the major ones, including Multilogin, AdsPower and GoLogin, collected on the integrations page.
What a Proxy Changes, and What It Can’t Hide
One layer out of manyA proxy replaces the network identity of your traffic. Everything above the network layer is produced by your client and arrives at the website unchanged.
What a proxy cannot hide or change:
- The TLS and HTTP/2 fingerprint. The handshake is tunneled end to end, so it is your client’s.
- Headers and Client Hints. Over HTTPS the proxy cannot read or rewrite them.
- Every JavaScript signal. Screen, fonts, GPU, canvas, audio,
navigator, timezone and language come from the browser. - WebRTC. Unless restricted, it can report your real IP alongside the proxy’s.
- Cookies, storage and logins. One account logged in from two exits links both sessions, whatever the fingerprint.
- Behavior. Timing and interaction patterns are yours.
You can see the split for yourself with a TLS echo service. Run the same request with and without the proxy: the reported IP changes, the TLS and HTTP/2 fingerprints do not.
# Quick check that the proxy works
curl -x http://USERNAME:[email protected]:5000 https://api.ipify.org
# Direct: your IP, curl's JA4 and HTTP/2 fingerprint
curl -s https://tls.peet.ws/api/all | jq '.ip, .tls.ja4, .http2.akamai_fingerprint_hash'
# Through the proxy: a different IP, the same JA4 and HTTP/2 hash
curl -s -x http://USERNAME:[email protected]:5000 \
https://tls.peet.ws/api/all | jq '.ip, .tls.ja4, .http2.akamai_fingerprint_hash'
Our guides on anonymous proxies and how to hide your IP address cover the same split from the user’s side. The proxy supplies a clean, correctly located exit; making the browser agree with it is your job.
Matching Timezone, Locale and Geolocation to the Proxy in Playwright
Derive the profile from the exit, then launchPlaywright can emulate the three location signals that most often contradict a proxy. On browser.new_context(), locale sets navigator.language, the Accept-Language header and Intl formatting; timezone_id sets the IANA timezone; geolocation plus permissions=["geolocation"] answers the Geolocation API with your coordinates. The options are documented in Playwright’s emulation guide.
Order matters. Look up the exit IP through a sticky session, then launch the browser through the same session. If the IP rotates in between, your matched timezone belongs to a different IP. Sticky sessions are explained in sticky vs rotating proxies and in the sticky sessions docs.
Step 1: profile the exit IP
This example asks ipinfo.io’s free JSON endpoint (rate-limited without a token) about the exit. In production use your own GeoIP database or a paid lookup; providers disagree near borders and for mobile ranges. If you need a specific city, target it in ProxyEmpire and set its timezone deliberately.
import os
from urllib.parse import quote
import requests
from playwright.sync_api import sync_playwright
GATEWAY = "v2.proxyempire.io:5000"
# Copy a sticky-session (Same IP) username from
# Proxy Manager -> Connection Builder in the ProxyEmpire dashboard.
PROXY_USER = os.environ["PROXYEMPIRE_USER"]
PROXY_PASS = os.environ["PROXYEMPIRE_PASS"]
# Extend for the countries you target. Pick the locale real users there run.
LOCALES = {
"US": "en-US", "GB": "en-GB", "CA": "en-CA", "DE": "de-DE",
"FR": "fr-FR", "ES": "es-ES", "IT": "it-IT", "BR": "pt-BR",
}
def exit_profile():
"""Ask a GeoIP service what the proxy's exit IP looks like."""
url = "http://{}:{}@{}".format(
quote(PROXY_USER, safe=""), quote(PROXY_PASS, safe=""), GATEWAY
)
resp = requests.get(
"https://ipinfo.io/json",
proxies={"http": url, "https": url},
timeout=30,
)
resp.raise_for_status()
info = resp.json()
lat, lon = (float(x) for x in info["loc"].split(","))
return {
"ip": info["ip"],
"country": info["country"],
"timezone": info["timezone"],
"locale": LOCALES.get(info["country"], "en-US"),
"geolocation": {"latitude": lat, "longitude": lon, "accuracy": 100},
}
Step 2: launch Chromium with the matching context
Use the http:// proxy URL here. Playwright’s Chromium rejects username and password authentication on SOCKS5 proxies (“Browser does not support socks5 proxy authentication”), and the ProxyEmpire gateway supports HTTP as well as SOCKS5. The launch flag restricts WebRTC to the proxy, as described above.
READ_SIGNALS = """() => ({
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
utcOffsetMinutes: -new Date().getTimezoneOffset(),
language: navigator.language,
languages: navigator.languages,
})"""
def run(profile):
with sync_playwright() as p:
browser = p.chromium.launch(
# Playwright's Chromium rejects SOCKS5 with username/password,
# so use the HTTP proxy URL.
proxy={
"server": "http://" + GATEWAY,
"username": PROXY_USER,
"password": PROXY_PASS,
},
# Keep WebRTC from sending UDP outside the proxy.
args=["--force-webrtc-ip-handling-policy=disable_non_proxied_udp"],
)
context = browser.new_context(
locale=profile["locale"],
timezone_id=profile["timezone"],
geolocation=profile["geolocation"],
permissions=["geolocation"],
)
page = context.new_page()
# 1. Same sticky session, so the browser should exit from the same IP.
page.goto("https://api.ipify.org")
browser_ip = page.inner_text("body").strip()
if browser_ip != profile["ip"]:
raise RuntimeError("exit IP changed: session expired or not sticky")
# 2. Read back what JavaScript sees.
signals = page.evaluate(READ_SIGNALS)
print(profile["country"], browser_ip, signals)
browser.close()
if __name__ == "__main__":
run(exit_profile())
Two notes from running this. Compare timezones by offset or normalized name, not raw strings: we passed Europe/Kyiv and Chromium reported the older alias Europe/Kiev. And granting geolocation lets every page read the coordinates without a prompt; if you don’t need location, leave geolocation and permissions out, since real users often deny it.
What this does not cover
Context options fix location signals only. The GPU string, fonts, the Chromium build’s TLS fingerprint and navigator.webdriver stay as they are. If a site offers an official API or data feed, that is usually more reliable than any browser setup. For running many such contexts, see our notes on designing a Playwright browser cluster, and for the protocol underneath, WebDriver BiDi vs CDP.
Browser Fingerprint Test Pages
Different tools answer different questionsA browser fingerprint test shows what a site can read from your setup. Each public tool measures something different, so use the one that matches your question and don’t compare scores across them.
| Test page | What it shows | Use it for |
|---|---|---|
| BrowserLeaks | Separate pages for IP, WebRTC, canvas, WebGL, fonts, TLS and more | Checking one layer at a time |
| CreepJS | Open-source research page that hunts for inconsistent or tampered values | Finding contradictions |
| Cover Your Tracks | EFF’s tool (formerly Panopticlick); tracker blocking and fingerprint uniqueness | Privacy posture of a normal browser |
| AmIUnique | Academic project comparing your attributes with its dataset | Seeing which attributes are rare |
| tls.peet.ws | JSON echo of your TLS, HTTP/2 and TCP/IP fingerprint | Comparing HTTP clients |
Three cautions. Uniqueness scores depend on each tool’s visitor population, which skews privacy-conscious, so “unique” there predicts little about any given website. Test in the exact configuration you run, including headless mode, extensions and flags; your everyday browser says nothing about a Playwright worker. And test through the proxy.
Privacy and Regulation
Fingerprinting is not a legal loopholeFingerprinting is harder for users to control than cookies, so regulators take it seriously. In the EU, the EDPB’s Guidelines 2/2023 on the technical scope of Article 5(3) of the ePrivacy Directive treat fingerprinting-style access to device information as within scope, so consent requirements can apply as they do for cookies. In the UK, the ICO responded in December 2024 to Google allowing fingerprinting by its advertising customers from 16 February 2025, calling the change irresponsible and stressing that data protection law, including PECR, applies. US state privacy laws such as California’s also treat persistent device identifiers as personal information.
Fraud prevention and security are widely recognized uses, and rules differ by jurisdiction and purpose. This is context, not legal advice; get advice for your own situation.
Frequently Asked Questions
Browser fingerprinting, in shortWhat is browser fingerprinting?
Identifying a browser from the characteristics it exposes (TLS handshake, headers, screen, fonts, timezone, canvas and WebGL output, network addresses) rather than from an identifier stored on the device. Many ordinary attributes combined can recognize a device across visits.
How is a fingerprint different from a cookie?
A cookie is stored in your browser and can be deleted. A fingerprint is recomputed on every visit, so clearing storage or opening a private window does not reset it.
Does a proxy stop browser fingerprinting?
No. A proxy changes the exit IP, ASN and location. TLS and HTTP/2 fingerprints, headers, JavaScript signals, WebRTC and behavior pass through unchanged, so the browser has to agree with the proxy.
What is a WebRTC leak?
WebRTC contacts a STUN server over UDP, which an HTTP proxy does not carry. The STUN server sees your real public IP, and any page can read it from the srflx candidate, so the site sees both the proxy IP and yours.
How do I run a WebRTC leak test?
Note your real IP with the proxy off, enable the proxy exactly as you use it, open a page such as browserleaks.com/webrtc and check that no WebRTC public IP is your real IPv4 or IPv6 address. Repeat after updates.
How do I stop WebRTC from leaking my IP in Chrome?
Set the WebRTC IP handling policy to disable_non_proxied_udp, either with the WebRtcIPHandling enterprise policy, the --force-webrtc-ip-handling-policy launch switch in automation, or an extension that offers WebRTC leak protection, such as the free ProxyEmpire Proxy Manager. Then confirm with a leak test.
Is canvas fingerprinting blocked by incognito mode?
Not in general. A private window has empty storage but the same fonts, graphics stack and GPU, so the canvas output is usually unchanged. Brave randomizes readouts; Tor Browser makes users report uniform values.
Give your browser an exit that matches it
Residential and mobile IPs with country, region and city targeting, sticky sessions for consistent profiles, and a free Chrome extension with WebRTC leak protection and matched timezone and language.



