Canvas Fingerprint
Your GPU and browser driver silently produce a different pixel pattern on every machine. Canvas fingerprinting exploits this by drawing styled text onto an offscreen canvas, exporting the pixel buffer, and hashing it into an identifier that survives cookie clears, VPN switches, and private browsing sessions. The mechanism is HTMLCanvasElement.toDataURL(): a script draws styled text and shapes onto an offscreen canvas, exports the raw pixel buffer, and hashes it. The hash captures every rendering difference introduced by the GPU model, graphics driver, OS compositor, and font rasterizer.1
Researchers documented this technique in 2012; it remains one of the highest-entropy passive signals available without any user interaction.2 The W3C fingerprinting guidance classifies canvas renders as active fingerprinting surfaces, noting that scripts deliberately draw shapes and text to elicit hardware-specific rendering differences that browsers cannot easily normalize.3
What is Canvas?
HTMLCanvasElement.toDataURL() to export a PNG-encoded pixel buffer from the browser's 2D rendering context.2 A script draws a fixed test composition: styled text, filled shapes, and gradients onto an offscreen canvas element, then hashes the raw pixel data. Because GPU drivers, OS compositors, and font renderers produce subtly different output for identical drawing commands, the resulting hash identifies the rendering hardware combination. Brave randomizes canvas output per site via its farbling mechanism.4 Firefox with privacy.resistFingerprinting set to true returns a modified canvas that degrades tracking precision without breaking legitimate canvas functionality.5How the browser generates the canvas hash
Generating a canvas fingerprint takes three steps. First, a script creates an offscreen HTMLCanvasElement and calls getContext('2d') to obtain a drawing context. Second, it draws a fixed test composition: styled text in multiple font sizes, filled rectangles with specific alpha values, shadow effects, and Unicode emoji characters. Third, it calls toDataURL('image/png') to export the raw pixel buffer as a Base64 string, then hashes that string. Consequently, the hash captures every rendering difference the GPU, driver, OS, and font system introduced during the draw pass.
Building on this, even two machines running the same browser version on the same OS frequently produce different hashes when their GPU drivers differ, because the ANGLE graphics translation layer on Windows produces driver-dependent pixel values for anti-aliased text and sub-pixel gradient rendering that no browser-level normalization step can eliminate.6
Why canvas hashing resists conventional spoofing
Spoofing a canvas fingerprint requires either randomizing the pixel buffer on every access or replacing the entire rendering pipeline with a fixed output. Extensions like CanvasBlocker add small amounts of random noise to the exported pixel data, which changes the hash on every page load.7 Yet this approach introduces a detectable anomaly: a canvas hash that changes on every access is itself a fingerprinting signal, because real browsers on real hardware produce consistent hashes across calls within a session.
Furthermore, fully blocking toDataURL() breaks legitimate applications including data URI exports, image editors, and some CAPTCHA systems, which makes total suppression impractical for general-purpose browsers. Brave solves this by injecting per-site random noise that is consistent within a session but differs across origins, which passes site functionality checks while preventing cross-site linking through a stable hash.
Detecting and blocking canvas fingerprinting in practice
Detecting a canvas fingerprint attempt requires monitoring calls to HTMLCanvasElement.toDataURL() and toBlob() for unusual behavioral patterns: a short test string rendered in multiple fonts, followed by an immediate data URI export with no visible output to the user. Chromium-based browsers expose a Canvas debugger panel in DevTools that logs every draw call and pixel export. Conversely, not all canvas exports indicate fingerprinting; games, chart libraries, image editors, and video processing tools call the same APIs for legitimate purposes.
How to choose the right blocking level
Effective blocking therefore targets the behavioral pattern rather than the API call itself. Firefox's privacy.resistFingerprinting intercepts toDataURL() at the engine level and injects a randomized modification to the pixel buffer before returning it to the script, making it more robust than extension-based approaches because it operates below the JavaScript layer and cannot be detected by checking whether the API responds normally.
You can test whether a blocking level is too aggressive by loading a site that depends on canvas rendering and watching for distorted charts or broken images. The goal is to add just enough noise to reduce the hash's uniqueness without making the page unusable. CapyToolkit lets you compare the exported value before and after each change so the trade-off stays visible.4
Emoji and Unicode as entropy amplifiers in canvas fingerprinting
Canvas fingerprinting scripts draw text rather than shapes because text rendering exposes more hardware-specific variance. Simple geometric shapes like filled rectangles and solid-color gradients produce pixel-perfect output across most hardware, because no font rasterizer or sub-pixel hinting algorithm is involved. Text rendering involves the OS font stack, the browser's text layout engine, the GPU's anti-aliasing pipeline, and the display's sub-pixel order, each introducing a layer of hardware-specific variation.
Emoji characters amplify this effect further. Emoji rendering calls platform-specific image compositing pathways: Apple's Core Text stack on macOS, DirectWrite on Windows, and FreeType with HarfBuzz on Linux each composite emoji differently. A script that includes a skin-tone emoji or a flag sequence in its canvas draw call exposes platform-specific rendering differences that purely ASCII text cannot produce. The combination of a Latin character string, a Unicode symbol, and an emoji creates a multi-layer rendering test that distinguishes hardware combinations that produce identical hashes for simpler inputs.
How gradient and shadow rendering add additional signal
Shadow blur radius calculations also vary by rendering backend. A script that draws text with a specified shadow blur using CanvasRenderingContext2D.shadowBlur exposes how each GPU and compositor handles Gaussian blur approximations. Chrome on Windows applies ANGLE's blur algorithm, which differs from Skia's algorithm used in Firefox. A canvas composition that combines emoji, shadow blur, and a semi-transparent gradient overlay creates a compound test that produces highly distinct output across GPU and browser combinations.
OffscreenCanvas and cross-origin isolation effects
OffscreenCanvas is a newer browser API that allows canvas rendering to happen in a Web Worker, off the main thread. Fingerprinting scripts use OffscreenCanvas because Web Worker execution is less visible to browser extensions that monitor main-thread canvas API calls. An extension that intercepts HTMLCanvasElement.toDataURL() on the main thread may not intercept the equivalent OffscreenCanvas call in a worker, depending on the extension's architecture.
The same GPU and rendering stack produces the same pixel output regardless of whether the script uses a standard HTMLCanvasElement or an OffscreenCanvas. Consequently, the fingerprint value collected through OffscreenCanvas is identical to the standard canvas fingerprint on the same machine, but the collection path is less accessible to content-script-based blocking. Firefox with privacy.resistFingerprinting applies its canvas modification at the rendering engine level, which applies consistently to both HTMLCanvasElement and OffscreenCanvas regardless of which thread initiates the draw call.
How cross-origin isolation changes canvas access permissions
Cross-origin isolation (achieved through Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy response headers) grants access to high-resolution timing APIs but does not restrict canvas fingerprinting. A cross-origin isolated page can use SharedArrayBuffer and high-precision performance.now(), which may help fingerprinting scripts time rendering operations more precisely. However, canvas access restrictions under cross-origin isolation apply to cross-origin iframes reading canvas contents from a parent frame, not to first-party canvas fingerprinting scripts operating within the same origin. The isolation boundary affects canvas data sharing between origins, not canvas data collection within a single origin.
Try in the tool
Where this shows up in the inspector
- CANVAS HASH row marked TRACKABLE; the hashed output of toDataURL() on this exact machine
- Documented since 2012, per the research cited on this page
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Wikipedia, "Canvas fingerprinting," accessed June 2026. https://en.wikipedia.org/wiki/Canvas_fingerprinting
- 2.
Mozilla Developer Network, "Canvas API," developer.mozilla.org, July 2025. https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
- 3.
Brave, "Fingerprinting defenses 2.0," brave.com, accessed June 2026. https://brave.com/privacy-updates/4-fingerprinting-defenses-2.0/
- 4.
Mozilla Developer Network, "privacy.websites," developer.mozilla.org, July 2025. https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/privacy/websites
- 5.
Electronic Frontier Foundation, "Cover Your Tracks | About," coveryourtracks.eff.org, accessed June 2026. https://coveryourtracks.eff.org/about
- 6.
kkapsner, "CanvasBlocker," github.com, accessed June 2026. https://github.com/kkapsner/CanvasBlocker/blob/master/README.md
- 7.
W3C, "Mitigating Browser Fingerprinting in Web Specifications," w3.org, September 2025. https://www.w3.org/TR/fingerprinting-guidance/
Canvas fingerprinting calls HTMLCanvasElement.toDataURL() or toBlob() to export the rendered pixel buffer. A script draws styled text and shapes onto an offscreen canvas, then hashes the exported PNG data. The hash reflects GPU, driver, OS, and font rendering differences across machines, not anything about you personally.
Chrome and Firefox use different rendering pipelines. On Windows, Chrome routes canvas rendering through ANGLE, while Firefox uses its own Skia-based pipeline. Each engine applies different anti-aliasing and sub-pixel rendering decisions for identical drawing commands, producing different pixel outputs and therefore different hashes.
Yes. Brave applies farbling to canvas output when Standard or Aggressive Shields are active. The farbling noise is consistent within a session but changes between sites and sessions, which prevents cross-site linking without breaking canvas-dependent applications like online image editors or CAPTCHA systems.
Partially. Extensions like CanvasBlocker inject random noise into toDataURL() output, changing the hash per page load. The limitation is that a constantly changing hash is itself detectable as a protection measure. CapyToolkit can help you see whether the canvas row changes after you enable the extension. For stronger coverage, use Firefox with privacy.resistFingerprinting or Brave with Shields enabled.
Not necessarily unique to you as a person, but often unique to your hardware combination. Two users with the same GPU model, driver version, OS, and browser version may share a canvas hash. Trackers treat each unique hash combination as identifying a specific browser across sessions, not a specific human identity.
WebGL Fingerprint
When a page needs graphics details, WebGL can reveal your GPU model directly from the graphics driver. The WEBGL_debug_renderer_info WebGL extension exposes two high-value parameters: UNMASKED_VENDOR_WEBGL, which returns the GPU manufacturer, and UNMASKED_RENDERER_WEBGL, which returns the full GPU model name, driver series, and rendering backend identifier1. Scripts retrieve these values by calling getParameter(ext.UNMASKED_VENDOR_WEBGL) and getParameter(ext.UNMASKED_RENDERER_WEBGL) after obtaining the extension object from a WebGL context. Consequently, a single pair of API calls returns a string like "ANGLE (NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0)" that directly identifies the GPU model, the Windows Direct3D version, and the shader model2, a triple of values that narrows a browser population dramatically. Furthermore, WebGL exposes dozens of additional parameters beyond the renderer string, each reflecting hardware capability limits that vary across GPU generations and vendor implementations.
What is WebGL?
WEBGL_debug_renderer_info extension to call getParameter(ext.UNMASKED_VENDOR_WEBGL) and getParameter(ext.UNMASKED_RENDERER_WEBGL) on a WebGLRenderingContext2. These calls return the GPU manufacturer string and the full renderer string, which includes the GPU model, rendering backend, and shader version. On Windows, Chrome routes WebGL through ANGLE and returns a renderer string that identifies the Direct3D version and GPU model directly3. Beyond the renderer, scripts also query parameters like gl.MAX_TEXTURE_SIZE, gl.MAX_VERTEX_ATTRIBS, and the list of supported WebGL extensions to build a multi-dimensional hardware profile.What the WEBGL_debug_renderer_info extension exposes
The WEBGL_debug_renderer_info extension is optional in the WebGL specification, meaning browsers choose whether to expose it4. Chrome and Firefox expose it by default; querying it requires calling canvas.getContext('webgl').getExtension('WEBGL_debug_renderer_info') and checking that the return value is not null. Calling getParameter(ext.UNMASKED_RENDERER_WEBGL) then returns a string that, on Windows Chrome, takes the form "ANGLE (NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0)"3. This string simultaneously identifies the GPU model, the rendering API (Direct3D11), the vertex shader model, and the pixel shader model, packing four distinct hardware identifiers into a single return value that any script can read without special permissions.
Why WebGL exposes so much
Consequently, each GPU generation and driver family produces a distinct renderer string. Furthermore, different operating systems produce different string formats for the same GPU: macOS reports Metal-based strings, Linux reports Mesa-based strings, and Windows reports ANGLE-based strings, making the renderer string an OS indicator as well as a GPU identifier. This level of detail is what makes WebGL fingerprinting so much more precise than simpler signals like screen resolution or timezone, which only narrow the population to thousands of browsers rather than pinpointing a specific hardware configuration that can single out one user among millions with a single API call.
The renderer string is worth checking because it reveals more than most users expect, including the GPU model and the operating system build in a single value. Running the inspector shows whether your browser returns a detailed string or a generic one, and whether the WebGL surface is present at all. That visibility helps you judge how much a given protection actually changes what a script can read.
Testing the WebGL row in CapyToolkit makes this concrete rather than theoretical. The inspector reports the exact renderer string your browser returns, so you can see whether a protection has replaced it with a generic value or removed the WebGL surface entirely. That direct read is the fastest way to confirm whether a privacy setting is actually reducing what scripts can collect.
Parameter fingerprinting beyond vendor strings
Beyond the renderer string, WebGL exposes more than 50 queryable parameters that reflect hardware capability limits specific to each GPU model, driver generation, and browser implementation4. gl.MAX_TEXTURE_SIZE indicates the maximum texture dimension the GPU supports; entry-level mobile GPUs typically report 4096, while desktop GPUs report 16384 or higher, and this single value alone can distinguish mobile from desktop hardware. gl.MAX_VERTEX_ATTRIBS reports how many vertex attributes the shader can handle simultaneously, and the list of supported WebGL extensions varies between GPU vendors, driver versions, and browser implementations in ways that create a distinctive multi-parameter signature. Yet the renderer string alone is not always sufficient for precise identification; two machines with the same GPU model but different driver versions may report identical renderer strings, which is why scripts query multiple parameters.
Building on this, combining the renderer string, the MAX_TEXTURE_SIZE limit, the supported extension list, and the shader precision format values produces a multi-dimensional hardware profile that reaches higher entropy than the renderer string alone, because each parameter adds an independent dimension that narrows the candidate hardware population in a way that no single value could achieve on its own. Fingerprinting scripts typically query 10 to 15 parameters in a single WebGL context initialization, building a profile that is substantially more identifying than any individual signal.
Mitigation options across browsers
Browser vendors have adopted different strategies for limiting WebGL fingerprinting exposure. Brave blocks the WEBGL_debug_renderer_info extension entirely when Shields are active; getExtension('WEBGL_debug_renderer_info') returns null, making the vendor and renderer strings unavailable5. Furthermore, Brave randomizes additional WebGL parameters at the extension level when Shields are on, so even parameters accessible without WEBGL_debug_renderer_info carry per-site noise.
When WebGL masking helps
Firefox with privacy.resistFingerprinting enabled spoofs the renderer string to a generic value and limits several capability parameters to prevent hardware-level profiling6. Tor Browser disables WebGL by default at the Safer security level, which eliminates the entire attack surface but prevents WebGL-dependent applications like 3D maps, games, and data visualizations from running7. Conversely, Chrome exposes the full WebGL surface without modification; users who want protection on Chrome must rely on third-party extensions, none of which replicate Brave's depth of coverage.
What protection costs in compatibility
If you use WebGL-heavy sites, test the exact page after changing browser settings because a protected browser may keep the application running while reducing the detail fingerprinting scripts can collect. That practical check is better than assuming every WebGL block is either safe or broken. Disabling WebGL entirely, as Tor Browser does at higher security levels, eliminates the fingerprinting surface but also breaks 3D maps, browser games, and data visualizations that depend on hardware-accelerated rendering. A more measured approach is to use Brave or Firefox with spoofing, which preserves WebGL functionality while limiting the specificity of the signals that scripts can read.
Try in the tool
What to look for
- Queryable WebGL parameters 50+
- Parameters scripts typically query 10 to 15 per context
- MAX_TEXTURE_SIZE, mobile GPUs 4096
- MAX_TEXTURE_SIZE, desktop GPUs 16384 or higher
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Khronos Group, "WebGL WEBGL_debug_renderer_info Extension Specification," registry.khronos.org, July 2014. https://registry.khronos.org/webgl/extensions/WEBGL_debug_renderer_info/
- 2.
Mozilla Developer Network, "WEBGL_debug_renderer_info extension," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_debug_renderer_info
- 3.
Chromium Project, "ANGLE — Almost Native Graphics Layer Engine," chromium.googlesource.com, accessed June 2026. https://chromium.googlesource.com/angle/angle/+/HEAD/README.md
- 4.
Mozilla Developer Network, "WebGLRenderingContext: getParameter() method," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/getParameter
- 5.
Brave Software, "Fingerprinting 2.0: WebGL getParameter handling," github.com, accessed June 2026. https://github.com/brave/brave-browser/issues/10214
- 6.
Mozilla, "Bug 1966860 — Enable WEBGL_debug_renderer_info extension and spoof vendor," bugzilla.mozilla.org, May 2025. https://bugzilla.mozilla.org/show_bug.cgi?id=1966860
- 7.
Tor Project, "Fingerprinting protections," support.torproject.org, accessed June 2026. https://support.torproject.org/tor-browser/features/fingerprinting-protections/
The WEBGL_debug_renderer_info extension exposes two parameters: UNMASKED_VENDOR_WEBGL returns the GPU manufacturer name, and UNMASKED_RENDERER_WEBGL returns the full GPU model, rendering backend, and shader version. On Windows Chrome, the renderer string includes the GPU model and Direct3D version, directly identifying your graphics hardware.
ANGLE (Almost Native Graphics Layer Engine) is the graphics translation layer Chrome uses on Windows to convert WebGL OpenGL ES calls to Direct3D. The ANGLE prefix in the renderer string is specific to Windows Chrome. macOS uses Metal, and Linux uses Mesa, producing different string formats for the same underlying GPU.
Firefox with privacy.resistFingerprinting enabled spoofs the WebGL renderer string to a generic value and masks certain capability parameters. Without RFP, Firefox exposes the same WEBGL_debug_renderer_info data as Chrome. Brave provides stronger coverage by blocking the extension entirely rather than spoofing the return values.
Yes. Even without the renderer string, WebGL exposes 50+ capability parameters like MAX_TEXTURE_SIZE, supported extensions, and shader precision formats that vary by GPU model and generation. Blocking WEBGL_debug_renderer_info reduces the most specific identifier but does not eliminate WebGL as a fingerprinting surface.
Brave blocks the WEBGL_debug_renderer_info extension entirely when Shields are active, returning null for the extension object. It also randomizes additional WebGL parameters with per-site noise. CapyToolkit can show whether the WebGL row disappears or changes after Shields are enabled. WebGL functionality remains available for legitimate applications, but the signals fingerprinting scripts rely on are suppressed or randomized.
AudioContext Fingerprint
For silent audio tracking, AudioContext fingerprinting hashes the output of a test audio signal1. A script creates an OfflineAudioContext, attaches an OscillatorNode set to a fixed frequency, connects the output to a DynamicsCompressorNode, and renders the result offline2. The rendered audio buffer is then passed to an AnalyserNode, and getFloatFrequencyData() exports the frequency response as a Float32Array. Hashing this array produces a value that reflects the floating-point arithmetic behavior of the CPU, the audio driver stack, and the OS audio subsystem. Chalise and Vadrevu documented this technique in their arXiv 2021 paper, noting that even machines with identical hardware can produce different audio fingerprints when their audio drivers differ1. Consequently, the audio hash is independent of the canvas hash, making them complementary signals: a tracker that collects both has higher confidence even when one hash is blocked or modified by the browser.
What is AudioContext?
AudioContext fingerprinting calls OfflineAudioContext to render a test signal in memory without producing audible output3. A script creates an OscillatorNode at a fixed frequency, routes it through a DynamicsCompressorNode, and starts the audio graph. When rendering completes, AnalyserNode.getFloatFrequencyData() exports the frequency response as a Float32Array. Hashing this array produces a value that reflects the CPU's floating-point rounding behavior, the OS audio stack, and the DAC characteristics. The OfflineAudioContext`` API performs this computation entirely in memory, without any speaker output, making it invisible to the user during normal browsing.How the audio hash is generated
Generating an audio fingerprint requires four steps. First, a script instantiates an OfflineAudioContext with a fixed sample rate and buffer length, typically OfflineAudioContext(1, 44100, 44100). Second, it creates an OscillatorNode set to a frequency like 10000 Hz and connects it through a DynamicsCompressorNode to the OfflineAudioContext destination. Third, it calls startRendering(), which executes the entire audio graph in memory without producing any audible output. Fourth, it creates an AnalyserNode, pipes the rendered buffer through it, and calls AnalyserNode.getFloatFrequencyData() to export the frequency response values. Hashing the resulting Float32Array produces a stable identifier.
Why AudioContext reveals hardware differences
Consequently, this entire process runs silently in the main JavaScript thread with no user interaction and no visible output, making it undetectable without specialized monitoring tools. Building on this, the computation completes in under 50 milliseconds on most hardware, adding negligible page load overhead2. The frequency response curve captures subtle variations in how each CPU handles the DynamicsCompressorNode's gain reduction algorithm, which involves iterative floating-point operations that produce slightly different rounding results across processor architectures and driver implementations
Running the same audio test on two physical machines with different CPUs shows how distinct the curves can be, even when the rest of the browser looks identical. The inspector reports the frequency response so you can see whether your browser randomizes it or leaves the hardware signature exposed. That single check reveals a signal most users never realize their device is broadcasting on every page load.4.
Testing the AudioContext row in CapyToolkit turns that invisible process into something you can see. The inspector shows the frequency response curve your device produces, so you can tell whether a protection is randomizing it or leaving the hardware signature fully exposed. Running the same test before and after enabling a protection is the clearest way to confirm the change actually took effect.
Why hardware differences appear in audio processing
The audio fingerprint varies across machines because floating-point arithmetic is not perfectly uniform across CPU architectures, DAC implementations, and audio driver stacks. The IEEE 754 floating-point standard permits implementations to apply extended precision in intermediate calculations, which means two CPUs can produce slightly different results for the same sequence of floating-point operations even when running identical code4. Consequently, the DynamicsCompressorNode's gain reduction calculations, which involve iterative floating-point operations on the audio buffer, produce subtly different output values on different hardware.
How protections change the audio curve
Furthermore, the audio driver stack introduces variation because the OS mixes signals at the driver level, and different audio API implementations, CoreAudio on macOS, WASAPI on Windows, and ALSA on Linux, apply different processing chains before the data reaches the JavaScript environment5. Building on this, even two machines with the same CPU model can produce different audio fingerprints when their audio drivers differ.
Blocking AudioContext fingerprinting
Blocking the AudioContext fingerprint is harder than blocking canvas because the same OfflineAudioContext API powers legitimate audio applications; synthesizers, audio worklets, and Web Audio-based games all depend on it. Brave injects per-site noise into the frequency response values exported by AnalyserNode.getFloatFrequencyData(), similar to its canvas farbling approach6. Firefox with privacy.resistFingerprinting enabled replaces the system math library in Web Audio with fdlibm, producing bit-identical audio output across platforms and preventing fingerprinters from exploiting cross-platform floating-point differences7.
Why a total block is rarely practical
Yet no universal browser block exists for the base AudioContext API; completely disabling the Web Audio API would break too many legitimate audio applications. Conversely, some extensions can intercept AudioContext calls and return spoofed values, but extension-based blocking operates at the JavaScript layer and can be bypassed by scripts that check for consistent API behavior. Combining Brave's built-in noise injection with uBlock Origin in strict mode provides the most practical level of protection currently available.
Try in the tool
What to look for
- Test oscillator frequency 10000 Hz
- OfflineAudioContext render call OfflineAudioContext(1, 44100, 44100)
- Typical render time under 50ms
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Chalise, Shekhar and Vadrevu, Phani, "A Study of Feasibility and Diversity of Web Audio Fingerprints," arxiv.org, July 2021. https://ar5iv.labs.arxiv.org/html/2107.14201
- 2.
Mostsevenko, Sergey, "Audio Fingerprinting: What It Is + How It Works with Web API," fingerprint.com, accessed June 2026. https://fingerprint.com/blog/audio-fingerprinting/
- 3.
Mozilla Developer Network, "OfflineAudioContext," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext
- 4.
IEEE Standards Association, "Differences Among IEEE 754 Implementations," grouper.ieee.org, accessed June 2026. https://grouper.ieee.org/groups/msc/ANSI_IEEE-Std-754-2019/background/addendum.html
- 5.
Mercer, Daniel, "AudioContext fingerprinting: the OscillatorNode signature explained," blog.crawlex.net, accessed June 2026. https://blog.crawlex.net/blog/audiocontext-fingerprinting/
- 6.
Brave Software, "Fingerprinting 2.0: Web Audio," github.com, accessed June 2026. https://github.com/brave/brave-browser/issues/9187
- 7.
Mozilla, "Bug 1358149 — Address fingerprinting issues with AudioContext," bugzilla.mozilla.org, accessed June 2026. https://bugzilla.mozilla.org/show_bug.cgi?id=1358149
AudioContext fingerprinting uses OfflineAudioContext to render a test audio signal offline, OscillatorNode to generate a fixed-frequency tone, DynamicsCompressorNode to process the signal, and AnalyserNode.getFloatFrequencyData() to export the frequency response as a Float32Array. Hashing that array produces the fingerprint value.
The DynamicsCompressorNode performs iterative floating-point calculations during signal processing. The IEEE 754 standard permits CPUs to apply extended precision in intermediate steps, causing subtly different rounding behavior across CPU models and audio driver implementations. This produces different Float32Array output values even for identical audio input.
No. AudioContext fingerprinting uses OfflineAudioContext, which renders the audio graph entirely in memory without routing output to speakers. The entire signal processing and hash generation happens silently in JavaScript. No browser permission is required and no audio is played to the user.
Yes. Brave injects per-site, per-session noise into the values returned by AnalyserNode.getFloatFrequencyData(). The noise is consistent within a single site session but changes across sites and sessions, preventing cross-site linking through a stable audio hash without breaking Web Audio-based applications.
Yes. CapyToolkit's Browser Fingerprint Inspector displays the audio fingerprint hash your browser produces. Comparing the value in a default browser versus Brave with Shields active or Firefox with RFP enabled shows whether audio fingerprinting protection is effective on your configuration.
User-Agent Fingerprint
The User-Agent string was the first fingerprinting signal in browsers. Every HTTP request carries a User-Agent header, and navigator.userAgent exposes the same string to JavaScript. The original purpose was content negotiation; servers needed to know which browser version to serve compatible content for. Consequently, the UA string evolved into a detailed identifier including the browser engine, version number, OS name, OS version, and architecture. Chrome 107 began the UA Reduction initiative, freezing the minor, build, and patch version numbers to "0.0.0"1, which reduces the entropy of the UA string component2. Building on this, navigator.userAgentData provides a structured replacement through the User-Agent Client Hints API, where Sec-CH-UA sends a low-entropy brand list by default and high-entropy values like platform version and architecture require explicit server requests via Accept-CH headers. The UA string alone provides moderate entropy; its tracking value increases when combined with other hardware signals.
What is User-Agent?
navigator.userAgent to extract the browser engine, version, OS name, and platform string. The same value is sent automatically in the Sec-CH-UA HTTP header on Chromium-based browsers as part of the User-Agent Client Hints API. Scripts call navigator.userAgentData.getHighEntropyValues()(['architecture', 'platformVersion', 'model']) to request additional structured data beyond the default low-entropy hint set3. The returned object contains platform, brand name, mobile status, and optionally the full version string, CPU architecture, and device model, each withheld by default unless the server requests it via the Accept-CH response header.What the legacy User-Agent string contains
The User-Agent string began as a simple identifier but grew into a multi-field browser descriptor. A typical Chrome 124 User-Agent on Windows reads: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"4. Parsing this string reveals the Windows NT 10.0 version, the 64-bit architecture, the WebKit compatibility marker, and the Chrome major version. Firefox on macOS reports a different structure that includes the Gecko rendering engine version, and Safari on both iOS and macOS uses a WebKit-based format that omits the Chrome-style compatibility tokens entirely, producing a shorter but equally identifying string.
Why the User-Agent still matters
Consequently, the UA string simultaneously encodes the browser type, the OS family, and the platform architecture, three separate signals in a single header. Building on this, before Chrome 107's UA Reduction initiative, the full build version was included, making even minor browser updates detectable from the UA string alone without any JavaScript execution required on the page. The practical consequence is that a single HTTP request fingerprint could identify a specific browser build among millions of users, amplifying the tracking value of every other signal collected during the session because the UA string was available before any JavaScript even executed on the page.
You can see the exact value your browser sends by opening the inspector and checking the User-Agent row, then compare it with the Client Hints values the same browser exposes. A browser with UA Reduction active shows a frozen build number, while one without it reveals the full version string on every request. Testing both side by side shows how much the reduction step actually removes from what a server can read.
UA Reduction and the Client Hints replacement
Chrome 107 introduced UA Reduction, which freezes the minor, build, and patch version components of the Chrome UA string to "0.0.0"1. The UA string still reports the major version and OS information, but the full build identifier is no longer exposed. The User-Agent Client Hints API (navigator.userAgentData) provides a structured, permission-gated replacement2.
How client hints change the fingerprint
By default, every request from a Chromium browser sends three low-entropy hints: Sec-CH-UA (the browser brand and major version), Sec-CH-UA-Mobile (whether it is a mobile browser), and Sec-CH-UA-Platform (the OS name). Yet servers that respond with an Accept-CH header listing Sec-CH-UA-Arch, Sec-CH-UA-Platform-Version, or Sec-CH-UA-Model can request the higher-entropy values that UA Reduction removed from the legacy string. Furthermore, JavaScript can call navigator.userAgentData.getHighEntropyValues() directly3, which returns the same high-entropy values without requiring a server round-trip.
Spoofing UA strings and why it rarely reduces entropy
Changing the User-Agent string through browser settings, extensions, or developer tools is straightforward, but it rarely reduces overall fingerprint entropy because the other signals remain unchanged. A browser reporting a Firefox UA string while still exposing a Chrome-specific canvas hash, ANGLE WebGL renderer, and Chrome extension list creates an internally inconsistent fingerprint profile. Fingerprinting systems score consistency across signals; an inconsistent profile is itself a distinctive signal, potentially easier to re-identify than an unmodified fingerprint.
Furthermore, spoofing the UA string on Chromium does not change the Sec-CH-UA header, which reports the real browser brand unless the server specifically targets Client Hints. Conversely, browsers like Brave take a different approach; they report a realistic UA for a generic version of the browser rather than attempting to impersonate a completely different engine, which avoids the consistency mismatch that UA spoofing creates.
Legacy navigator properties that accompany the User-Agent string
Beyond navigator.userAgent, the browser exposes several related properties that fingerprinting scripts collect alongside the UA string. navigator.platform returns a short OS identifier such as "Win32", "MacIntel", or "Linux x86_64"; this string is reduced by Chrome's UA Reduction but still distinguishes OS families. navigator.language returns the browser's primary language setting, such as "en-US" or "fr-FR". navigator.languages returns the full ordered language preference list set in the browser's language settings.
Taken together, navigator.userAgent, navigator.platform, navigator.language, and navigator.languages form a four-signal language and OS cluster. The combination of "Win32" platform plus "en-US" language plus a Chrome UA string narrows the browser population to English-speaking Windows Chrome users; adding navigator.languages that includes "es" or "zh-CN" narrows it further. Firefox with privacy.resistFingerprinting spoofs navigator.platform to a platform-dependent value ("Win32" on Windows, "MacIntel" on macOS, "Linux x86_64" on Linux) rather than using a single universal string, because a platform_value that mismatches other signals would itself be a detectable fingerprint5.
Chrome's UA Reduction program includes navigator.platform alongside navigator.userAgent and navigator.appVersion as one of the three primary reduced APIs6. The property now returns a frozen generic value based on the OS family rather than the real hardware identifier. A Chrome 124 browser on a 64-bit Windows machine reports "Win32" from navigator.platform while the UA string reports "Windows NT 10.0; Win64; x64", creating a subtle mismatch between the two signals that persists even after UA Reduction shipped.
How Brave and Firefox handle User-Agent differently
Brave and Firefox take opposite approaches to User-Agent privacy. Brave reports a realistic User-Agent string for a generic Chrome version rather than attempting to impersonate a different browser engine. When Shields are active, Brave's reported UA matches a recent but non-specific Chrome version on a generic Windows platform, which places the browser within the large population of Chrome users without creating a detectable spoofing pattern. This approach avoids the consistency mismatch that arises when a browser reports a Firefox UA while its canvas hash and WebGL renderer string are clearly Chrome-specific.
Firefox with privacy.resistFingerprinting takes a stricter approach. RFP spoofs the User-Agent to a specific pinned string that all Firefox RFP users share, freezing both the browser version and the OS platform. Consequently, all Firefox RFP users appear to have the same browser version and platform, which provides herd anonymity at the cost of potentially misrepresenting the browser version to sites that use the UA for compatibility decisions.
What Sec-CH-UA exposes that the legacy UA string cannot be changed to hide
Sec-CH-UA sends the browser brand list on every request from Chromium browsers, including Brave. Brave's UA string may report a generic Chrome version, but the Sec-CH-UA header sends the "Brave" brand alongside the Chromium brand, because the Client Hints API was designed to be accurate rather than privacy-preserving7. You can verify which brands your browser sends in Sec-CH-UA by opening DevTools, checking a request's headers, and looking for the Sec-CH-UA field. The brand list identifies the real browser even when the legacy UA string is spoofed.
Try in the tool
The three default Client Hints
- Sec-CH-UA browser brand and major version
- Sec-CH-UA-Mobile whether it is a mobile browser
- Sec-CH-UA-Platform the OS name
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Chromium Project, "User-Agent Reduction," www.chromium.org, February 2022. https://www.chromium.org/updates/ua-reduction/
- 2.
WICG, "User-Agent Client Hints," wicg.github.io, February 2026. https://wicg.github.io/ua-client-hints/
- 3.
Mozilla Developer Network, "NavigatorUAData: getHighEntropyValues() method," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues
- 4.
Ali Beyad, Karl Dubost, and Milica Mihajlija, "Chrome and Firefox soon to reach major version 100," web.dev, February 2022. https://web.dev/articles/chrome-and-firefox-soon-to-reach-major-version-100
- 5.
Mozilla, "nsRFPService.h," searchfox.org, accessed June 2026. https://searchfox.org/mozilla-central/source/toolkit/components/resistfingerprinting/nsRFPService.h
- 6.
Google Privacy Sandbox, "User-Agent Reduction: Android model and version," privacysandbox.google.com, May 2022. https://privacysandbox.google.com/blog/user-agent-reduction-android-model-and-version
- 7.
MDN, "Sec-CH-UA header," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-CH-UA
The User-Agent string is a legacy HTTP header and JavaScript property (navigator.userAgent) that encodes browser engine, version, OS, and architecture in a single string. Client Hints (navigator.userAgentData, Sec-CH-UA) replace this with a structured API where low-entropy values are sent by default and high-entropy values require explicit server or script requests.
Chrome 107 froze the minor, build, and patch version numbers in the UA string to 0.0.0, preventing per-build browser tracking. The major version, OS family, and architecture remain visible. This change reduced UA string entropy modestly but did not affect canvas, WebGL, or audio fingerprinting signals.
Rarely. Spoofing the UA string creates a mismatch with other browser signals like canvas hash, WebGL renderer, and plugin list, which fingerprinting systems detect as an anomaly. An inconsistent profile can be more distinctive than an honest one. Brave reports a generic but consistent UA rather than impersonating a different browser.
Sec-CH-UA sends the browser brand list and major version (e.g., "Chrome";v="124"), Sec-CH-UA-Mobile sends a boolean for mobile detection, and Sec-CH-UA-Platform sends the OS name. These three low-entropy hints are sent on every Chromium request without any server permission request.
Firefox does not implement Client Hints and continues sending the legacy UA string. Safari also does not send Sec-CH-UA headers. On Chromium browsers, privacy.resistFingerprinting in Brave or Firefox spoofs the UA to a generic value. CapyToolkit shows the UA value your browser exposes, but Chrome does not provide a user-facing toggle to disable Sec-CH-UA headers.
Timezone Fingerprint
Timezone is a low-entropy signal that narrows geographic location. Intl.DateTimeFormat().resolvedOptions().timeZone returns the IANA timezone identifier such as "America/New_York" or "Europe/Berlin", while Date.prototype.getTimezoneOffset() returns the UTC offset in minutes.1 Neither API requires any user permission, and both execute synchronously in any JavaScript context.
Taken alone, a timezone identifier narrows a user to one of roughly 400 IANA zones, representing modest entropy.2 Yet timezone becomes meaningful when combined with other low-entropy signals: the browser language, the Accept-Language header, and the installed font set. Consequently, the combination of UTC-5 offset plus en-US language plus a Windows-specific font set collapses to a much smaller population than any of those signals individually.
Furthermore, a VPN does not change the reported timezone; a user connecting through a Japanese exit node who reports "America/New_York" creates a detectable mismatch that fingerprinting systems flag as a VPN indicator.3 This makes timezone one of the simplest signals to collect yet one of the hardest to spoof without also changing the underlying operating system locale settings.
What is Timezone?
Intl.DateTimeFormat().resolvedOptions().timeZone, which returns the IANA timezone string from the operating system locale settings. The complementary API, Date.prototype.getTimezoneOffset(), returns the UTC offset in minutes as a signed integer, negative for timezones east of UTC. These two APIs provide different granularities: the IANA string identifies the exact zone including daylight saving time transitions, while getTimezoneOffset() provides only the current UTC offset, which is shared by many zones simultaneously.4 Both calls are synchronous and require no permission. The timezone changes only when the user manually adjusts their system clock region or OS locale settings.How timezone is detected through two API paths
Two separate JavaScript APIs expose timezone information, each with different characteristics. Intl.DateTimeFormat().resolvedOptions().timeZone returns the full IANA timezone identifier, "America/Los_Angeles", "Asia/Tokyo", or "Europe/London", which encodes both the UTC offset and the daylight saving time transition rules specific to that zone. Date.prototype.getTimezoneOffset() returns the current UTC offset in minutes as a signed integer; -300 represents UTC-5 and 330 represents UTC+5:30.
Why timezone is a quiet identifier
Consequently, a script that calls both APIs gets complementary data: the IANA string identifies the exact zone while the offset confirms the current UTC difference. Furthermore, the IANA timezone changes when the user adjusts their OS regional settings, but it does not change when they connect through a VPN, use a private browsing window, or clear cookies. Building on this, the getTimezoneOffset() value fluctuates by an hour during daylight saving time transitions, which means two readings taken months apart can differ for zones that observe DST.
You can check what your browser reports by opening the inspector and reading the timezone row, then compare it with your actual system setting to see whether they match. A browser with resistFingerprinting enabled reports UTC regardless of where you are, which removes the geographic clue entirely. Running the same check on a VPN connection shows the mismatch that fingerprinting services look for when they try to spot tunneled traffic.
Timezone combined with language and locale
Timezone achieves its highest tracking value when correlated with other geographic and cultural signals. UTC-5 combined with Accept-Language: en-US reduces the candidate pool to English speakers in the US Eastern timezone, tens of millions of users. Yet adding Windows-specific system fonts, a 1920x1080 screen, and a mid-range NVIDIA GPU further narrows this pool to a specific socioeconomic segment.
When timezone becomes meaningful
Building on this, unusual timezone-language combinations are particularly distinctive. A browser reporting "Asia/Tokyo" while setting Accept-Language to "en-US" is statistically rare and may indicate a traveler, expat, or VPN user. Conversely, a user reporting "Europe/Paris" plus "fr-FR" language is less distinctive because many millions share that combination. Furthermore, multiple-timezone environments, a user who frequently travels and changes their system clock, produce inconsistent timezone histories across sites that record and compare timezone reports over time.
Spoofing timezone and the VPN mismatch problem
Firefox with privacy.resistFingerprinting enabled freezes the reported timezone to UTC regardless of the system locale setting, which eliminates geographic information from the timezone signal entirely.5 Scripts that call Intl.DateTimeFormat().resolvedOptions().timeZone receive "UTC", and Date.prototype.getTimezoneOffset() returns 0 for every Firefox RFP user worldwide. Consequently, the timezone signal contributes zero geographic information when RFP is active, making all Firefox RFP users appear to share the same global location.
How privacy browsers handle timezone
Brave does not randomize the timezone by default; Standard Shields focuses on higher-entropy signals like canvas and WebGL rather than low-entropy locale indicators. Yet the most significant timezone spoofing problem occurs with VPN users who do not also adjust their system timezone. A browser reporting "America/Chicago" while connecting from a European VPN exit node creates a geographic inconsistency that fingerprinting services actively check. Building on this, some privacy tools offer automatic timezone adjustment to match the VPN's apparent location, which eliminates the mismatch signal but requires additional configuration beyond basic VPN setup.
Try in the tool
What to look for
- IANA timezone count roughly 400 zones
- Firefox RFP timezone frozen to UTC
- Firefox RFP getTimezoneOffset() returns 0
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Mozilla Developer Network, "Intl.DateTimeFormat.prototype.resolvedOptions()," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions
- 2.
"List of tz database time zones," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- 3.
Kameleo, "Timezone Fingerprinting," kameleo.io, 2024. https://kameleo.io/blog/timezone-fingerprinting
- 4.
Mozilla Developer Network, "Date.prototype.getTimezoneOffset()," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset
- 5.
Mozilla, "Bug 1330890 — Spoof timezone as UTC," bugzilla.mozilla.org, 2017. https://bugzilla.mozilla.org/show_bug.cgi?id=1330890
Intl.DateTimeFormat().resolvedOptions().timeZone returns the full IANA timezone identifier such as "America/New_York". Date.prototype.getTimezoneOffset() returns the current UTC offset in minutes. Both APIs are synchronous and require no user permission. They read the timezone from the OS locale settings rather than from any stored browser data.
No. A VPN changes your IP address and apparent network location, but it does not modify your OS timezone setting. Your browser continues to report the system timezone regardless of which VPN server you connect through. A mismatch between your VPN location and your reported timezone is a detectable signal in fingerprinting systems.
Firefox with privacy.resistFingerprinting enabled freezes the timezone to UTC for all scripts. This eliminates the geographic information the timezone signal provides. Brave does not farble timezone by default. CapyToolkit can show the timezone value your browser reports before and after you change settings. The most portable approach is to manually set your OS timezone to UTC, which works in all browsers without any configuration.
Alone, timezone is low-entropy, roughly 400 IANA zones. Combined with browser language, Accept-Language header, screen resolution, and font set, it becomes a meaningful corroborating signal that narrows the candidate population. Unusual combinations like a non-English language with a US timezone are particularly distinctive.
Date.prototype.getTimezoneOffset() changes value during daylight saving time transitions, by plus or minus 60 minutes depending on the zone. The IANA timezone string itself does not change during DST; it encodes the DST rules. A script that reads getTimezoneOffset() in summer versus winter will see different values for the same machine.
Screen Resolution Fingerprint
The pixel dimensions your browser reports are more identifying than they appear. A raw 1920x1080 resolution is shared by hundreds of millions of users, yet fingerprinting scripts combine screen.width, screen.height, window.devicePixelRatio, and screen.colorDepth into a multi-value profile that is far less common than any single number suggests.
Unusual combinations, a 3840x2160 resolution at devicePixelRatio 1.75, or a setup that produces a screen.width larger than any standard single monitor, dramatically narrow the candidate pool. HiDPI and non-integer-scaling setups that are common among developers and design professionals create distinctive device signatures as a result.
Furthermore, iOS normalizes the reported CSS resolution independently of the physical display resolution, which means mobile device fingerprinting based on screen resolution differs fundamentally from desktop fingerprinting approaches.1
What is Screen Resolution?
screen.width and screen.height for the total monitor dimensions in CSS pixels, window.devicePixelRatio for the display scaling factor, and screen.colorDepth for the color bit depth. screen.availWidth and screen.availHeight report the available space minus OS taskbars. On high-DPI displays, devicePixelRatio greater than 1 indicates a Retina or HiDPI screen where one CSS pixel maps to multiple physical pixels. All four APIs are synchronous, require no permission, and are available in every JavaScript context.2 screen.width and screen.height report the primary monitor's dimensions on multi-monitor setups.What resolution and DPI reveal about your hardware
The four screen APIs together describe the display hardware class with considerably more precision than any single signal alone suggests, because the combination of resolution, pixel density, color depth, and available area creates a multi-dimensional profile. screen.width at 3840 and screen.height at 2160 identifies a 4K monitor, a configuration shared by a much smaller user population than the hundreds of millions using 1920x1080. window.devicePixelRatio at 2.0 indicates a Retina display typical on Mac hardware, while a value of 1.5 indicates a specific display scaling setting in Windows that is significantly less common in the global browser population.3
Why screen size is more specific than it looks
Consequently, a 4K monitor at 1.75 scaling (devicePixelRatio: 1.75) is a statistically unusual combination that narrows the population more than either value alone. Building on this, screen.colorDepth at 24 bits is near-universal, but some displays report 30 bits for HDR content, which is distinctive. Furthermore, screen.availWidth smaller than screen.width by more than 30 pixels may indicate a visible taskbar of a specific height, which correlates with OS identity on platforms where taskbar height is standardized.
You can see the exact values your display reports by opening the inspector and reading the screen resolution, devicePixelRatio, and color depth rows together. A standard 1920x1080 monitor at ratio 1.0 looks ordinary, while a 4K panel at a non-integer scaling stands out immediately. Comparing two of your own devices side by side shows how much the combination narrows the pool compared with any single dimension on its own.
HiDPI and multi-monitor setups as entropy amplifiers
High-DPI displays significantly amplify the fingerprinting contribution of screen signals because the combination of physical resolution and scaling factor creates distinctive CSS pixel dimensions that are shared by fewer users. A macOS user with a 2560x1440 external monitor at devicePixelRatio 2.0 reports screen.width 1280 and screen.height 720 in CSS pixels, a combination that signals the exact display model class when combined with the macOS-specific User-Agent string. Windows users who set display scaling to 150% report devicePixelRatio 1.5, and those at 175% report 1.75; these non-integer values are considerably less common than 1.0 or 2.0, which narrows the candidate pool from hundreds of millions to tens of millions of browsers worldwide.
How browser protections change the view
Conversely, multi-monitor setups typically appear only as the primary monitor's dimensions in screen.width and screen.height, which means a user with two monitors appears no different from a user with one unless the secondary monitor has a different devicePixelRatio. Building on this, screen resolution combined with GPU model from WebGL identifies the display hardware tier with high precision. Firefox with privacy.resistFingerprinting rounds the reported dimensions to the nearest 200px increment, masking the true monitor resolution while keeping the values plausible enough that most layout scripts continue to function normally.4
Why resolution matters less on mobile than desktop
iOS deliberately normalizes the CSS resolution reported to the browser, independent of the physical screen resolution. An iPhone 15 Pro with a physical resolution of 2556x1179 reports screen.width 430 and screen.height 932, the logical resolution at 3x device pixel ratio. Consequently, many iPhone models with different physical screens report similar CSS dimensions, which reduces the tracking value of screen resolution on iOS. Android does not apply the same normalization; Android Chrome reports the actual CSS pixel dimensions, which vary more widely across the fragmented Android device ecosystem.5
What to compare before changing settings
Furthermore, desktop browsers report different values at different monitor zoom levels; a user who sets Chrome to 90% zoom changes the effective screen.width reported to scripts, which can cause session-to-session inconsistency. Building on this, iOS 26 AFP normalizes screen dimensions further as part of its fingerprinting protection suite, making iOS 26 Safari report less distinctive screen information than earlier iOS versions.
Try in the tool
What to look for
- Firefox RFP rounding nearest 200px, minimum 1000x900
- iPhone 15 Pro physical resolution 2556x1179
- iPhone 15 Pro reported CSS resolution 430x932 (3x devicePixelRatio)
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Apple Inc., "Displays," developer.apple.com, accessed June 2026. https://developer.apple.com/library/archive/documentation/DeviceInformation/Reference/iOSDeviceCompatibility/Displays/Displays.html
- 2.
Mozilla Developer Network, "Screen.width," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/Screen/width
- 3.
Mozilla Developer Network, "Window.devicePixelRatio," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
- 4.
Mozilla, "Bug 1330882 — When privacy.resistFingerprinting = true, set new windows to rounded dimensions," bugzilla.mozilla.org, 2017. https://bugzilla.mozilla.org/show_bug.cgi?id=1330882
- 5.
Peter-Paul Koch, "More about devicePixelRatio," quirksmode.org, July 2012. https://quirksmode.org/blog/archives/2012/07/more_about_devi.html
screen.width and screen.height return the total monitor dimensions in CSS pixels. window.devicePixelRatio returns the display scaling factor (2.0 for Retina, 1.5 for 150% Windows scaling). screen.colorDepth returns the bit depth. screen.availWidth and screen.availHeight return the usable area excluding taskbars. All are synchronous and require no permission.
1920x1080 alone is low-entropy because hundreds of millions of users share it. Combined with devicePixelRatio and colorDepth, it becomes more useful as a corroborating signal. Unusual combinations like 3840x2160 at devicePixelRatio 1.75 are more distinctive because fewer users share that specific configuration.
Usually not. screen.width and screen.height report the primary monitor dimensions on multi-monitor setups, not the combined desktop width. A user with two 1920x1080 monitors reports the same screen.width as a user with one. The secondary monitor only appears in fingerprint data if the browser window moves to it and triggers a devicePixelRatio change.
iOS uses CSS logical resolution, which normalizes the physical pixels by the device pixel ratio. An iPhone with a 3x display reports CSS dimensions one-third of the physical resolution. This normalization is a deliberate Apple design choice that also limits the fingerprinting value of screen resolution on iOS devices.
Firefox with privacy.resistFingerprinting rounds screen.width and screen.height to the nearest 200px with a minimum of 1000x900, masking your real display dimensions. CapyToolkit can show the screen row before and after you apply that setting. Changing OS display scaling also changes devicePixelRatio. Some browsers let you set a custom viewport width, but this affects layout rather than screen.width.
Hardware Concurrency Fingerprint
For device-tier tracking, hardware concurrency exposes your CPU core count to any script. navigator.hardwareConcurrency returns the number of logical CPU cores available to the browser, which on modern hardware typically ranges from 2 to 32 depending on the processor and hyperthreading configuration.1 The companion API, navigator.deviceMemory, reports the device's approximate RAM in gigabytes rounded to a small set of values: 0.25, 0.5, 1, 2, 4, or 8.2 Together, these two values describe the performance tier of the device, a combination that correlates with device price, purchase year, and user segment. Consequently, a 16-core machine with 8 GB reported memory identifies a mid-range to high-end desktop or workstation class. Building on this, these signals alone produce lower entropy than canvas or WebGL fingerprinting, but they function as strong corroborating signals when combined with screen resolution and GPU renderer to classify devices by tier and vintage.
What is Hardware Concurrency?
navigator.hardwareConcurrency returns the number of logical CPU cores as an integer. On a quad-core processor with hyperthreading, it returns 8. navigator.deviceMemory returns the device RAM in gigabytes rounded to the nearest power of 2, then clamped to implementation-defined bounds that typically cap between 2 and 32 GB.3 Both APIs are synchronous and available without any user permission in all modern browsers. The hardwareConcurrency value reflects both the physical core count and hyperthreading or simultaneous multithreading configuration, which means it varies based on CPU model and BIOS settings.What these values reveal about CPU and RAM class
The combination of navigator.hardwareConcurrency and navigator.deviceMemory enables device tier classification. An entry-level Android phone reports hardwareConcurrency 4 or 8 with deviceMemory 1 or 2. A mid-range laptop reports hardwareConcurrency 8 to 16 with deviceMemory 8. A workstation with a 24-core processor reports hardwareConcurrency 24 or 48 (with hyperthreading) paired with deviceMemory 8 (the maximum reported value).
Why CPU and memory hints matter
Consequently, the specific combination narrows the device class significantly within any site's audience. Building on this, high hardwareConcurrency values are increasingly common as consumer CPUs ship with more cores, which reduces the uniqueness of the high end. Yet specific combinations like hardwareConcurrency 12 (a 6-core hyperthreaded mobile CPU) remain distinctive because they identify a specific generation of mid-range notebook processors. Furthermore, these values are stable across sessions and do not change when the user switches networks, browsers, or VPN providers.
You can read both values directly by opening the inspector and checking the hardware concurrency and device memory rows, then compare them with the specifications of your actual machine. A browser with resistFingerprinting enabled reports a frozen core count, while one with farbling shows a value that shifts between sessions. Running the check on two different browsers on the same computer makes the difference in how each vendor handles the signal obvious.
How browsers handle these APIs differently
Browser vendors have diverged on how much accuracy to provide for these hardware APIs. Chrome exposes the real hardwareConcurrency value and the real deviceMemory in the permitted range. Firefox with privacy.resistFingerprinting enabled freezes hardwareConcurrency to 2 regardless of actual CPU count, which makes Firefox RFP users appear to have a 2-core machine, rare on modern hardware but consistent across all Firefox RFP users, providing herd anonymity.4
Brave randomizes hardwareConcurrency to a value between 2 and the true value on default protection, or between 2 and 8 on max protection, changing per session when Shields are active to prevent stable cross-site linking without completely misrepresenting the hardware.5 Conversely, Safari reports the real hardwareConcurrency value and does not farble it in current releases. Building on this, the discrepancy between what browsers report creates a secondary signal: a machine that always reports hardwareConcurrency 2 is likely running Firefox RFP, which is itself an identifier within the global browser population.
Using these signals to distinguish device tiers
Combining hardwareConcurrency and deviceMemory with screen resolution and GPU renderer produces a robust device tier classifier that fingerprinting services use for fraud detection and audience segmentation. A device reporting hardwareConcurrency 4, deviceMemory 2, screen.width 360, and a Mali GPU renderer is almost certainly an entry-level Android phone. A device reporting hardwareConcurrency 16, deviceMemory 8, screen.width 2560, and an NVIDIA RTX renderer is a high-end desktop workstation.
How protections change hardware signals
Consequently, these classifications are useful for fraud detection because bots and automation environments frequently misreport or cannot access accurate hardware values; headless Chrome in a cloud container typically reports hardwareConcurrency matching the container CPU allocation, which may be 2 or 4 rather than a value characteristic of a real consumer device. Firefox with privacy.resistFingerprinting freezes the reported value to 2, which collapses all RFP users into a single herd and prevents device tier classification entirely. Brave randomizes within the set {2, 4, 8} per session, which preserves plausible device profiles while preventing stable cross-site tracking of the hardware tier signal.
Why corroborating signals matter
Building on this, device tier signals also help ad networks apply audience segmentation based on inferred purchasing power correlated with device class. A user with hardwareConcurrency 16, deviceMemory 8, and an NVIDIA RTX renderer is classified as a high-value audience segment, while a user reporting hardwareConcurrency 2 and deviceMemory 1 falls into a budget-device category. The combination of multiple low-entropy signals produces a more reliable classification than any single API call, which is why fingerprinting scripts collect hardware concurrency alongside screen resolution, GPU renderer, and installed fonts rather than relying on any one value alone.
Try in the tool
What to look for
- Firefox RFP hardwareConcurrency frozen to 2
- Brave Shields hardwareConcurrency randomized between 2 and true value (2-8 on max protection)
- deviceMemory reported values 0.25, 0.5, 1, 2, 4, or 8 GB
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
Mozilla Developer Network, "Navigator.hardwareConcurrency," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency
- 2.
Mozilla Developer Network, "Navigator.deviceMemory," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory
- 3.
Mozilla, "Bug 1360039 — Spoof navigator.hardwareConcurrency = 2 when privacy.resistFingerprinting = true," bugzilla.mozilla.org, 2017. https://bugzilla.mozilla.org/show_bug.cgi?id=1360039
- 4.
Brave Software, "Fingerprinting 2.0: hardwareConcurrency," github.com, accessed June 2026. https://github.com/brave/brave-browser/issues/10808
- 5.
W3C, "Device Memory API," w3.org, accessed June 2026. https://www.w3.org/TR/device-memory/
navigator.hardwareConcurrency returns the number of logical CPU cores as an integer. On a 6-core processor with hyperthreading, it returns 12. On a 4-core phone processor without hyperthreading, it returns 4. The value reflects both physical core count and simultaneous multithreading configuration.
navigator.deviceMemory returns the device RAM class in GB, rounded to one of the values in {0.25, 0.5, 1, 2, 4, 8}. Devices with more than 8 GB of RAM report 8. The API is not implemented in Firefox or Safari, which return undefined. Chrome, Edge, and Brave (when Shields are off) return the real class value.
Firefox with privacy.resistFingerprinting enabled freezes navigator.hardwareConcurrency to 2, regardless of actual CPU core count. This makes all Firefox RFP users appear to have the same CPU configuration, providing anonymity through uniformity. Without RFP, Firefox returns the real core count.
When Brave Shields are active, navigator.hardwareConcurrency returns a randomized value between 2 and the true core count on default protection, or between 2 and 8 on max protection. The value changes per session, which prevents cross-session CPU tier tracking without freezing to a single implausibly low number.
Alone, it provides low entropy; many devices share the same core count. Combined with navigator.deviceMemory, screen resolution, GPU renderer, and User-Agent, it contributes meaningfully to device tier classification. CapyToolkit shows these values together so you can see the full device-tier picture. Fingerprinting systems use it primarily as a corroborating signal rather than a primary identifier.
WebRTC IP Leak
WebRTC is the browser API most likely to expose the real network path, especially when you rely on a VPN. RTCPeerConnection uses ICE (Interactive Connectivity Establishment) to discover candidate network paths for peer-to-peer connections. During this process, it calls STUN servers to discover the public IP address and enumerates local network interfaces to discover LAN IP addresses.
Critically, it creates its own UDP socket outside the VPN tunnel for these discovery requests, which means the public IP returned by a STUN server reflects the real network interface, not the VPN exit node.1 Consequently, a user connected through a VPN who opens a site that triggers `RTCPeerConnection.createOffer() may expose their real home IP address even though all HTTP traffic routes through the VPN. Building on this, the leak is not limited to the public IP; local LAN addresses like 192.168.x.x are also exposed in ICE` candidates, which can identify the router subnet and confirm that the user is on a private network.
What is WebRTC?
RTCPeerConnection's ICE candidate gathering process. A script creates an `RTCPeerConnection(), calls createOffer(), and listens for onicecandidate` events. Each candidate event includes a string containing an IP address discovered during network interface enumeration and STUN server queries. The server-reflexive candidate string for a STUN-discovered address takes the form "candidate:x x UDP 2130706431 [public-ip] [port] typ srflx". For local addresses, the form is "candidate:x x UDP 2122260223 [lan-ip] [port] typ host". Both strings reveal real IP addresses that bypass VPN tunneling on most platforms.2How WebRTC discovers local and public IPs
The ICE candidate gathering process runs automatically when a script creates an RTCPeerConnection and calls createOffer(), without any user permission or visible browser notification to alert the person browsing the page. The browser opens UDP sockets on each available network interface and sends STUN binding requests to a configured STUN server, which then replies with the public IP address that the request arrived from. Simultaneously, the browser enumerates all local network interfaces and includes each discovered LAN IP address as a host candidate in the ICE candidate list.
Why WebRTC can leak network clues
Consequently, a site that triggers this process receives both the real public IP from the STUN response and the LAN IP from the local interface enumeration, even if the user is connected through a VPN. Building on this, the LAN IP leak reveals the router subnet and confirms the presence of a private network; a 10.0.0.x LAN address pattern identifies a corporate network more specifically than a 192.168.1.x pattern that is typical of home routers.3 The entire ICE candidate gathering sequence completes within a few seconds of page load.
You can check whether your setup leaks by opening the inspector and running the WebRTC test, which gathers ICE candidates and shows the IP addresses your browser exposes. A VPN that tunnels UDP correctly hides your public address, while one that does not leaves the real IP visible in the candidate list. Running the test with the VPN both off and on makes the difference immediately clear and shows whether your configuration actually protects you.
Why VPNs don't block WebRTC by default
Most VPN implementations tunnel TCP connections through the virtual network interface, which causes HTTP traffic to appear to come from the VPN exit node. Yet WebRTC uses UDP for its ICE candidate discovery requests, and many VPN clients on Windows and macOS do not route UDP traffic through the VPN tunnel by default. The OS-level routing table determines which interface handles each UDP packet; if the VPN client does not insert a catch-all UDP route for all traffic, the STUN request travels over the physical network interface rather than the VPN interface.
How browser controls change WebRTC exposure
Consequently, the STUN server sees the real public IP rather than the VPN exit IP and returns it in the STUN binding response, which the browser includes as a server-reflexive ICE candidate. Firefox lets you disable WebRTC entirely through about:config by setting media.peerconnection.enabled to false, which stops all ICE candidate gathering at the cost of breaking video conferencing.4 Brave takes a more targeted approach by restricting ICE candidates to the active network interface, which prevents the physical interface from leaking when a VPN is active while keeping WebRTC functional for peer-to-peer applications.
Why connection order matters
Furthermore, the timing of VPN connection setup matters: if the VPN connects after the browser has already initialized a WebRTC session, the already-discovered candidates retain the real IP even after the VPN connects. This means that opening a WebRTC-enabled page before activating your VPN can expose your real public IP for the entire browsing session. The safest practice is to connect to the VPN first, then open the browser, ensuring that all ICE candidate gathering happens through the tunneled interface from the start.
Disabling WebRTC by browser
Browser vendors have adopted widely different approaches to WebRTC IP leak prevention, ranging from complete disabling to targeted candidate restriction depending on the vendor's assessment of the compatibility trade-off. Firefox allows complete disabling of WebRTC through about:config by setting media.peerconnection.enabled to false, which prevents all ICE candidate gathering at the cost of also disabling WebRTC-dependent features like browser-based video conferencing and peer-to-peer file sharing.
Brave blocks WebRTC from leaking non-VPN IP addresses by default; it still allows WebRTC functionality but restricts ICE candidates to addresses associated with the active network interface, preventing the physical interface from being exposed when a VPN is active. Safari and iOS Safari have blocked the WebRTC local IP leak since iOS 14 by using mDNS tokens instead of real IP addresses for host candidates, so sites see a token like a3f4b5c6.local rather than 192.168.1.x.5 Conversely, Chrome on desktop and Android does not restrict WebRTC IP exposure by default; users who want protection must install an extension like WebRTC Network Limiter or uBlock Origin with WebRTC leak prevention enabled.
Try in the tool
Where the leak is blocked, by browser
- Firefox media.peerconnection.enabled = false in about:config disables WebRTC entirely
- Brave restricts ICE candidates to the active network interface, on by default
- Safari / iOS since iOS 14, uses mDNS tokens instead of real LAN IPs for host candidates
Open the Browser Fingerprint & Privacy Leak Inspector tool to try this yourself.
Open the tool →- 1.
"WebRTC," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/WebRTC
- 2.
J. Uberti and G. Shieh, "WebRTC IP Address Handling Requirements," RFC 8828, IETF, January 2021. https://www.rfc-editor.org/rfc/rfc8828.html
- 3.
Philipp Hancke and Chad Hart, "Apple's not so private relay fails with WebRTC," webrtchacks.com, November 2021. https://webrtchacks.com/apples-not-so-private-relay-fails-with-webrtc/
- 4.
Mozilla, "Bug 1314443 — Audit the existing disable WebRTC preferences and ensure they work as advertised," bugzilla.mozilla.org, 2016. https://bugzilla.mozilla.org/show_bug.cgi?id=1314443
- 5.
Jocelyn Liu, "WebRTC Custom Settings," github.com, accessed June 2026. https://github.com/brave/brave-browser/wiki/WebRTC-Custom-Settings
WebRTC uses RTCPeerConnection ICE candidate gathering to discover network paths for peer-to-peer connections. CapyToolkit's WebRTC row shows whether those addresses are exposed to the page, which helps you verify whether your VPN or browser settings are effective. Because WebRTC uses a separate UDP path that bypasses most VPN tunnels, it reveals the real public IP and LAN IP even when a VPN is active.
Not automatically. Most VPNs tunnel TCP traffic but may not route UDP traffic through the VPN interface. WebRTC uses UDP for STUN requests, so the STUN server response may reflect the real IP rather than the VPN exit IP. A full-tunnel VPN that routes all UDP traffic and blocks split tunneling prevents the leak; most consumer VPNs do not guarantee this.
Set media.peerconnection.enabled to false in about:config. This disables all WebRTC functionality, which also stops video conferencing through browser-based tools. For a less disruptive option, setting media.peerconnection.ice.default_address_only to true prevents local IP exposure while keeping WebRTC functional for video calls.
Yes, by default. Brave restricts ICE candidate gathering to the network interface currently in use, preventing the physical interface from being exposed when a VPN is active. This protection is on by default in Standard Shields mode and does not require any user configuration.
Since iOS 14, Safari and iOS WebKit use mDNS token substitution for WebRTC host candidates. Instead of the real LAN IP (192.168.1.x), ICE candidates contain a token like a3f4b5c6.local. The real IP is never sent to the web page. Server-reflexive candidates from STUN still expose the public IP.