Canvas, WebGL and Audio Fingerprinting: How a Browser Gives Itself Away Without Cookies

Canvas, WebGL, Audio and video fingerprinting measure rendering and media-processing traits and turn them into a probabilistic browser identifier.
Unlike a cookie, this signal is not written into site storage. A script calls ordinary web APIs, collects their responses and compares them with earlier visits. For anti-fraud it is a risk signal; for privacy it is a reason to control what a profile exposes. A browser fingerprint is not a person's passport. It can change after a driver update, while the combined signals can still link sessions.
One Canvas hash proves nothing. Systems compare text metrics, pixels, GPU data, AudioContext, time zone, language and session behavior. Inconsistency between them is often more informative than any single value, and that inconsistency is exactly what most modern bot detectors are built around.
What Canvas, WebGL, Audio and video APIs measure
Fingerprinting measures environment properties rather than reading a stored ID. Responses are normalized, combined with other features and hashed. The same script can return different bytes on two machines because of the OS, fonts, driver or engine version.
Canvas fingerprinting reads pixels or geometry after drawing text and shapes. Canvas fingerprinting is sensitive to fonts and the text-rendering stack. WebGL adds extensions, precision formats, vendor/renderer values and the output of 3D rendering. A WebGL fingerprint can come from API parameters and from a buffer read after shader execution. Audio fingerprinting commonly uses OfflineAudioContext, while video fingerprinting relies on codecs, MediaCapabilities and WebCodecs. Each of these four vectors has its own "physical" reason to differ between devices, which is why they deserve separate treatment before being folded into one score.
| Fingerprint method | What it actually measures | Approximate entropy (bits) | Persistence across restart / updates | How the script collects it (API) |
|---|---|---|---|---|
| Canvas 2D | text rasterization, kerning, hinting, anti-aliasing | 5–8 bits | high, changes only after a font, OS or GPU driver update | toDataURL(), getImageData() |
| WebGL / WebGL2 | GPU vendor/renderer, shader precision, ANGLE backend | 6–10 bits | high, tied to physical hardware and the driver | getParameter(), readPixels(), WEBGL_debug_renderer_info |
| AudioContext | oscillator and compressor DSP processing, floating-point rounding | 3–5 bits | medium, depends on the browser's audio engine version | OfflineAudioContext, Float32Array buffer |
| WebCodecs / MediaCapabilities | codec support, hardware decoding, color gamut | 4–7 bits | medium, changes with a GPU driver or OS update | decodingInfo(), isConfigSupported() |
These ranges are approximate and follow the logic of studies such as Panopticlick and AmIUnique: the exact figure always depends on the specific user population rather than being a universal constant. The article returns to this table later, when it explains what a "bit of entropy" actually means in practice.
How text rendering and GPU math create a measurable trace
Text rendering in a browser passes through several independent OS subsystems. On Windows, glyph rasterization is handled by DirectWrite, on Linux typically by FreeType, and on macOS by CoreText. Each implements hinting, kerning and subpixel anti-aliasing differently, assigning different intensities to the red, green and blue subpixels of the display. The raster image then passes through color correction: a monitor's ICC profile and the sRGB working space can shift channel brightness slightly before pixels ever reach getImageData().
Chromium itself draws 2D Canvas through the Skia graphics library, and different Skia builds or Chromium versions can round letter outlines or apply anti-aliasing slightly differently. The GPU driver adds another layer: hardware-accelerated compositing depends on the driver version and the GPU model. Drawing the same line of text on two formally identical machines but with different GPU drivers is like asking two different people to sign with the exact same pen: the letters look similar, but the slant, pressure and tiny curves at the level of ten-thousandths of a pixel always differ slightly.
A script can draw mixed alphabets, emoji and a gradient, then call getImageData(). If p_i is one pixel channel, the measurement is v = (p_1, p_2, …, p_n) and a key can be stored as H(v || metadata). Hashing only makes comparison convenient. Anti-fraud often uses vector distance or clustering rather than exact hash equality.
// Safe example for an internal test page
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.font = '16px sans-serif';
ctx.fillText('Afina Ω 123', 8, 24);
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
console.log('sample length', pixels.length);
This demonstrates measurement mechanics. Production systems should not send raw pixels across domains or decide fraud from one rendering result.
WebGL and WebGL2 under the microscope: the engine, precision and ANGLE
WebGL adds a separate layer of graphics-pipeline data on top of Canvas. The WEBGL_debug_renderer_info extension exposes two parameters, UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL, which reveal the real GPU vendor and model instead of a generic string like "Google Inc." At the same time, gl.getParameter() exposes dozens of hardware traits: MAX_TEXTURE_SIZE, MAX_VERTEX_UNIFORM_VECTORS, ALIASED_LINE_WIDTH_RANGE and other limits tied to the specific chip and driver version.
Another dimension is computational precision inside shaders. gl.getShaderPrecisionFormat() reveals how a GPU rounds highp float in the fragment shader: the IEEE 754 standard defines the base rules for double- and single-precision floating point arithmetic, but a mobile GPU and a desktop GPU round the last digits differently in their concrete implementations. These microscopic differences accumulate across calculations and show up in the result of readPixels() after a test scene is rendered.
Technically, WebGL calls never touch the hardware directly; they pass through ANGLE, a layer that translates WebGL commands into the native graphics API of the specific platform: Direct3D 11 on Windows, Metal on macOS, increasingly Vulkan on Android and Linux, or OpenGL as a fallback. That is why UNMASKED_RENDERER_WEBGL can read something like "ANGLE (NVIDIA, Direct3D11 vs_5_0 ps_5_0)". If that backend does not match the claimed operating system, for example a Direct3D11 string under a macOS User-Agent, it is a critical mismatch: macOS physically does not use Direct3D. Anti-bot systems catch such mismatches instantly, before any behavioral analysis even begins. A WebGL renderer string only makes sense alongside the rendering output, the OS and the declared browser, never as a standalone value.
Why AudioContext and video fingerprinting add separate signals
OfflineAudioContext produces a deterministic signal that is still not identical across all stacks. An OscillatorNode generates a wave of a given shape, a DynamicsCompressorNode compresses its amplitude using a nonlinear algorithm, and the browser's engine returns a Float32Array buffer with no access to a microphone or real sound whatsoever: everything happens in memory, in an offline rendering mode that runs faster than real time.
The resulting numbers are shaped by the specific DSP pipeline implementation in the engine (Blink, WebKit or Gecko), the CPU architecture and the set of SIMD instructions available: AVX and AVX2 on modern x86 chips, or NEON on ARM. These instruction sets process several samples at once, but the order and method of summing floating-point values can differ, and that is exactly what ends up being read from the buffer. Instead of transmitting the entire array, a system typically extracts aggregates: individual samples, the sum of signal magnitudes, spectral peaks or a quantized sequence of values.
Video APIs expose a different class of data. MediaCapabilities.decodingInfo() returns an object with three fields, supported, smooth and powerEfficient, for a specific combination of codec, resolution and bitrate, while WebCodecs' VideoDecoder.isConfigSupported() checks support for a specific H.264, VP9, AV1 or HEVC profile before a decoder is even created. Hardware decoding depends on the chip and licensing: HEVC support on desktop, for instance, often requires a separate system codec, while hardware-accelerated AV1 only appears on newer GPUs. A separate class of signal is Wide Color Gamut support: the P3 and Rec.2020 color spaces can be checked through matchMedia('(color-gamut: p3)'), and their availability correlates with the display and GPU class. A missing codec or narrow gamut is not proof of automation; a managed device image or a build policy can explain it just as well.

Entropy is not uniqueness. It reduces the size of a group of similar clients. A rare GPU can be informative in a large population and nearly useless in a fleet of identical corporate laptops.
Shannon entropy: why a fingerprint is a probability, not an identity
In information theory, Shannon entropy is calculated as H = -Σ p_i · log2(p_i), where p_i is the probability of a specific feature value across the whole user population. The rarer a value, the more bits it adds to the overall fingerprint. One bit of entropy doubles the number of possible combinations: 10 bits yield roughly 1,024 groups, 20 bits already exceed a million.
That is exactly why no single vector from the table above works as a unique identifier on its own. A Canvas hash carrying 6 bits of entropy splits all users into roughly 64 groups; it is only useful in combination with other signals. Real scoring sums the entropy of Canvas, WebGL, Audio, fonts, time zone and language, and only the combined figure can approach practical uniqueness within a given site. Even then, this is probabilistic clustering, "this client is very likely the same one as yesterday", not cryptographically exact identification. Understanding that difference matters both for anti-fraud, which should not ban on the match of a single rare feature, and for privacy, where losing a cookie does not mean losing the ability to link sessions.
Blocking, noise injection or a consistent profile: what anti-fraud sees
Blocking an API returns an error or empty value. It limits measurement, but an unusual failure can itself become a rare signal: a blocked WebGL context is far less common in a real user population than a working one. Noise injection changes a response on every call. A consistent profile, by contrast, presents stable values that fit the OS, GPU and browser, and that is exactly the approach hardware emulation in Afina uses.

| Approach | How it works | What the site / WAF sees | Detection risk | Verdict |
|---|---|---|---|---|
| blocking calls / empty data | the API returns an error, undefined or an empty buffer | a missing response where 99% of real browsers return one | high: the absence of data itself is a rare, easily filtered signal | not recommended against a serious anti-bot |
| random noise injection | every Canvas/Audio call returns slightly different values | an unstable result on repeated measurement within the same page | very high: variance is exposed statistically within a few requests | worse than blocking, since it adds a new unique pattern |
| consistent hardware emulation (the Afina approach) | Canvas, WebGL, Audio and OS parameters are internally consistent and stable across sessions | stable, plausible values matching the declared device | low, provided the whole profile stays internally consistent | recommended approach for QA and legitimate multi-accounting |
Naive noise is exposed by statistics. If low bits change independently across five Canvas calls, hash variance and Hamming distance rise. If only toDataURL changes while getImageData, WebGL and CSS measurements disagree, channels conflict. A defender can repeat the call on the same page and test pixel correlation, and any instability where real hardware is stable immediately raises the risk score.
The critical mismatch matrix: when a profile gives itself away
Trying to swap out just one parameter, for instance through an ordinary browser extension that only changes navigator.platform, almost always makes a profile more conspicuous: every other signal, fonts, GPU, time zone, audio engine, still belongs to the real hardware and starts contradicting the swapped value. Cloudflare, DataDome, Akamai and similar systems build detection precisely around finding such internal conflicts rather than around a single "bad" value.
| Signal A | Signal B | Example conflict | Why it triggers detection |
|---|---|---|---|
| User-Agent = macOS | UNMASKED_RENDERER_WEBGL contains "Direct3D11" | a Direct3D11 ANGLE backend is physically impossible on macOS, which uses Metal | direct contradiction between the declared OS and the graphics engine |
navigator.platform = Win32 | text rendering matches a FreeType pattern | Windows typically uses DirectWrite, not the Linux font stack | mismatch between the text-rendering subsystem and the declared OS |
| time zone America/New_York, desktop User-Agent | the WebGL vendor string names a mobile chip (for example Mali or Adreno) | mobile GPUs are not installed in desktop systems | mixing a mobile and a desktop hardware profile |
hardwareConcurrency = 24 cores | User-Agent describes a budget mobile device | budget mobile chips do not carry 24 logical cores | unrealistic CPU configuration for the claimed device |
MediaCapabilities confirms hardware AV1 | UNMASKED_RENDERER_WEBGL points to an old GPU model without an AV1 block | hardware AV1 decoders only appeared in newer chip generations | conflict between the claimed decoding capability and the real hardware |
Every row in the table shows the same principle: changing one parameter without synchronously changing every signal linked to it creates a conflict more noticeable than the original value it was supposed to hide.
How to validate a Chromium profile without JavaScript patches
JavaScript patches can leave artifacts in property descriptors, function toString(), error order, main frames, workers or iframes. A safer architecture configures the Chromium mechanisms that actually produce the API: the graphics backend, font inventory, media capabilities and isolated profile storage.
In Afina, an isolated Chromium profile combines fingerprint settings, proxy, cookies and cache. Match OS, engine, time zone, languages, CPU, RAM and WebGL/Canvas/Audio settings in one profile instead of changing them after a page loads. See fingerprint management. A profile should remain stable across authorized sessions unless a real event, such as a browser update, explains a change.
- create a test profile with a complete parameter set;
- run Canvas, WebGL, Audio and media measurements three times in a row;
- compare stability in the main frame, iframe and worker;
- check the consistency of signal pairs against the matrix above, especially the ANGLE backend and the declared OS;
- repeat the tests after a Chromium update and record which values changed and which stayed stable;
- document the baseline and any differences in your ticket tracker before the profile goes into production use.
This is quality testing for your own product or authorized workflow, not a method for bypassing service rules or challenge pages.
How privacy and anti-fraud should interpret a fingerprint
Fingerprinting is useful with session reputation, request rate, network consistency and behavior. A sensible score allows uncertainty: a new monitor or driver does not equal account takeover. For your own automation, isolate profiles, avoid mixing cookies, document the purpose and use official APIs and platform terms.
How anti-fraud compares unstable measurements
In real scoring, a fingerprint should not be one opaque string. It is more useful to store separate components and their quality: whether an API is available, whether the result repeats, how far a new value sits from a historical cluster, and whether it is consistent with the configuration. A changed Canvas hash after a GPU driver update looks different from a simultaneous change in language, screen, fonts, GPU and time zone within a single minute.
Scoring is often described with the formula risk = Σ w_i f_i, where f_i is a feature and w_i is its weight. For fingerprinting it is better to think in conditional probabilities: how normal is this combination for a given device class and this particular account. Canvas weight should not stay constant. If an API is blocked by browser policy, that fact needs to be interpreted separately rather than being replaced with a fabricated hash.
A practical validation protocol for engineering teams
The baseline measurements from the previous section deserve to become a regular team practice rather than a one-off check. Capture the Chromium version, OS, GPU, driver, fonts and the results of control tests in a shared log, then reproduce the run in a clean profile, in the same profile the next day, and after every planned engine update. Compare new snapshots against previous ones with an automated diff script rather than by hand: that provides the data most one-off "browser checks" are missing.
Do not merge user fingerprints with full personal data without a legal basis and a retention period. A hash can itself be an identifier if it is used for re-identification. Defensive products should limit access to raw signals, pseudonymize records and separate fraud detection from analytics.
Where technical protection meets data policy
Reducing fingerprinting does not remove transparency obligations. Define which APIs a feature actually needs, when consent applies, how it is explained in plain language, and how related identifiers are deleted. Anti-fraud needs boundaries too: a list of permitted signals, manual review for high-risk cases and monitoring of false positives. A regular script audit helps catch a new API call before it becomes an invisible product dependency, and access to logs should be limited by role with retention periods defined before a feature ships. A security team should check whether the share of disputed decisions grows after every model or browser engine change. This material is educational and is not advice on bypassing service checks or challenge pages.
Afina keeps profile settings alongside working data rather than a collection of random plugins. Assign an owner for profile-change review, access control and periodic signal review. It also helps to check fallback fonts, GPU availability and codec changes after every Chromium update.
DownloadFAQ — Frequently Asked Questions
What is Canvas fingerprinting?
Canvas fingerprinting measures pixels or text metrics after Canvas rendering. Fonts, OS settings, drivers and display parameters create the differences.
Are Canvas fingerprints cleared with cookies?
No. Clearing cookies does not change the graphics stack. An OS, browser, font or driver update can change the result.
How does a WebGL fingerprint differ from Canvas?
WebGL adds GPU data, the ANGLE backend and 3D shader rendering output. Canvas more often measures 2D pixels and text rasterization.
Is AudioContext fingerprinting audible?
Usually not. OfflineAudioContext processes the signal in memory and the page reads a numeric buffer rather than microphone audio.
Why is random Canvas noise a poor defense?
Random noise makes repeated measurements unstable. That variance becomes an unusual profile signal itself.
Does fingerprint substitution guarantee passing anti-fraud?
No. Anti-fraud evaluates many signals and each service has its own policy. Profiles must be internally consistent and used only in authorized scenarios.
What does Shannon entropy mean in the context of fingerprinting?
It is the number of bits a given feature adds toward distinguishing users. More bits mean a smaller group of similar clients, not a guaranteed unique device.
Why is a WebGL renderer and User-Agent mismatch so easy to detect?
The ANGLE backend depends on the real OS, so a string like Direct3D11 under a macOS User-Agent is physically impossible. Anti-bot systems look for exactly this kind of internal contradiction between signals.
