Skip to content
On this page

WebGL fingerprinting reads identifying information directly from the GPU. WebGL is the browser feature that lets web pages draw 3D graphics using your graphics card, and a fingerprinting script asks it questions that real hardware answers in revealing ways. The browser exposes the graphics card vendor and renderer string (via WEBGL_debug_renderer_info), the list of supported WebGL extensions, the maximum texture size, the precision of shader operations, and - most distinctively - the pixel output of a known render operation (drawing a fixed image and comparing the exact pixels that come out). The combination is roughly tied to your hardware and survives most user-agent spoofing (faking the browser's identity string), so it keeps pointing at the same machine.

Quick facts

ReadsGPU vendor + renderer string, ~70 extension flags, render output hash
Distinct from CanvasWebGL probes the GPU directly; Canvas probes the 2D drawing pipeline
Common bot tellSwiftShader renderer (Google Inc. SwiftShader) = headless Chrome with no GPU
Block-grade signalRenderer mismatched to platform — e.g. NVIDIA on macOS, llvmpipe on Windows
DefeatsVanilla headless Chrome, default Playwright/Puppeteer

What WebGL exposes

The browser API exposes four classes of identifying data:

  1. Renderer string — the GPU's name, read via gl.getParameter(gl.RENDERER) with the WEBGL_debug_renderer_info extension. Returns strings like "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0, D3D11)" on Windows or "Apple GPU" on macOS.
  2. Extension list — about 70 named extensions (EXT_color_buffer_float, OES_texture_float_linear, etc.), the optional features your GPU and driver support. The exact set varies by GPU model and driver version.
  3. Parameter values — limits the hardware reports, such as max texture size (4K/8K/16K), max viewport dimensions, max vertex attributes, and fragment shader precision (how accurate the GPU's per-pixel maths is). Each varies by hardware tier.
  4. Rendered output — draw a known shape with a known shader (a small GPU program) and hash the pixels read back from the canvas into a short ID. Different GPUs produce subtly different floating-point rounding, anti-aliasing (edge smoothing), and gradient interpolation, so the ID is steady but machine-specific.

Why headless browsers fail WebGL the hardest

A headless browser runs with no visible window, the usual setup for automation. Headless Chrome without a GPU falls back to SwiftShader, Google's software rasterizer - a stand-in that draws graphics on the CPU instead of a real GPU. The renderer string is then "Google Inc. SwiftShader" or "Google SwiftShader" — anti-bot vendors block this string unconditionally because no real desktop user has SwiftShader as their primary GPU. The fallback chain is even worse on Linux: llvmpipe (Mesa's software rasterizer) is an instant tell.

Running headless Chrome with --use-gl=angle --use-angle=swiftshader-webgl still produces a SwiftShader renderer. The mitigations are: (1) run with xvfb (a virtual display) plus a real GPU passed through, (2) spoof WEBGL_debug_renderer_info at the CDP level (Chrome's remote-control protocol) with a believable renderer string, or (3) use a tool like Camoufox or CloakBrowser that patches the renderer at the C++ level (inside the browser engine itself).

The renderer-platform coherence trap

Spoofing the renderer string alone is insufficient. Anti-bot vendors cross-check the claimed renderer against the platform (navigator.platform, navigator.userAgent) and the GPU's expected extension list - in other words, do all the clues agree with each other? A request claiming Windows + Chrome 131 with renderer string "Apple GPU", or macOS with an NVIDIA RTX renderer, gets blocked. The renderer extension set must also match the claimed hardware — a budget integrated GPU advertising the extensions only found on enthusiast cards is a clear tell.

Camoufox's approach is to maintain a database of real (platform, GPU, renderer, extensions, parameters) tuples harvested from real users, and serve a coherent one per session, so every value matches. This is more expensive than spoofing individual values, which is why DIY hardening of WebGL almost always trips at least one cross-check.

Five ways to ask the same GPU the same question

The renderer string is the best-known WebGL signal and the least reliable one, because it is a single value that is easy to change. The checks that carry weight compare independent readings of the same hardware.

ComparisonWhy they must agree
WebGL 1 vs WebGL 2Two contexts, one physical GPU - the unmasked renderer must be identical in both
Main thread vs WorkerOffscreenCanvas gives a Worker a real context, reading the GPU from a separate realm
WebGL vs WebGPUTwo graphics APIs describing one adapter - vendor and tier must match
Renderer string vs driver limitsA named GPU implies a known range for texture size, uniform vectors, and extension list
Renderer string vs shader backendThe shading-language dialect and precision behaviour follow the actual backend in use

The last two are the most demanding, because they test whether the hardware named in a string can actually do what that hardware does. A context claiming a current discrete GPU while reporting a maximum texture dimension typical of a software rasteriser has made a claim its own capabilities contradict. Likewise, advertised extensions should be instantiable - listing an extension that fails when requested is a list that was not produced by the driver it claims to come from.

Readback integrity: what the pixels themselves prove

A separate family of checks ignores metadata entirely and examines rendered output for internal consistency. These target per-session output perturbation - the practice of adding small variations to canvas and WebGL readings so the resulting hash differs between visits.

The probes are deliberately trivial. Clear a buffer to a single flat colour and read it back: every pixel must be exactly that colour. Read the same region through two different paths - readPixels and toDataURL, or getImageData on overlapping sub-rectangles - and the overlapping pixels must be identical. Draw the same flat colour twice, once by clearing and once through a trivial shader, and the results must match.

Uniform noise fails these immediately, because a flat fill stops being flat. Noise applied at one readback path but not another fails the cross-path comparison. The underlying issue is that perturbation has to be indistinguishable from real rendering variation, but real rendering has no variation at all for a flat clear - the correct answer is exactly one value, and any deviation is arithmetically wrong rather than merely unusual.

The same logic extends to text and layout measurement, where advance widths must be additive across repeated glyphs and metrics must scale consistently, and to audio, where rendering silence must produce exact zeros. In every case the check is not "is this value rare" but "is this value arithmetically possible", which is why output perturbation has aged poorly as an approach compared with rendering honestly on hardware whose capabilities match the identity being presented.

Code example

javascript
// What an anti-bot script reads to fingerprint WebGL
function webglFingerprint() {
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl');
  if (!gl) return 'no-webgl';   // already suspicious

  const debug = gl.getExtension('WEBGL_debug_renderer_info');
  const renderer = debug
    ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL)
    : gl.getParameter(gl.RENDERER);

  return {
    renderer,                                                 // 'ANGLE (NVIDIA RTX 4070 ...)' or 'SwiftShader'
    vendor: gl.getParameter(gl.VENDOR),
    extensions: gl.getSupportedExtensions().sort().join(','), // ~70 entries
    maxTexture: gl.getParameter(gl.MAX_TEXTURE_SIZE),         // 4096 / 8192 / 16384
    maxViewport: gl.getParameter(gl.MAX_VIEWPORT_DIMS).join('x')
  };
}
// SwiftShader anywhere in the renderer string is a hard block at major vendors.

Next in Rendering and hardware surfaces · 2 of 8

The modern API that exposes far more structured detail.

What Is WebGPU Fingerprinting?

Related terms

Concept map

Concept map

How WebGL Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Is WebGL fingerprinting more or less reliable than Canvas fingerprinting?

More reliable for catching headless browsers — SwiftShader is an instant tell that canvas fingerprinting alone cannot catch. But it is less reliable for telling real users apart, because most people have one of a small set of common GPUs (integrated Intel, or Apple GPU on Mac), so many genuine visitors share the same value (high collision rates).

Can I just disable WebGL to avoid the fingerprint?

No — disabling WebGL is itself a strong signal. On a modern browser, calling getContext("webgl") is expected to return a working context. Returning null or throwing an error is what scrapers and Tor Browser do, so anti-bot vendors block that behavior.

What renderer string should a spoofed browser use?

A common real one that matches your claimed platform. The most-used renderers in production traffic are "ANGLE (Intel, Intel UHD Graphics ...)" on Windows, "Apple GPU" on macOS/iOS, and "Mali-G78" on mid-range Android. Pick one consistent with the User-Agent and don't change it within a session.

Why does adding noise to canvas and WebGL output get detected?

Because some rendering operations have exactly one correct result. Clearing a buffer to a flat colour must produce that colour in every pixel, and reading the same region through two different APIs must produce identical bytes. Noise makes a flat fill non-uniform or makes two readings of one region disagree, which is arithmetically impossible rather than statistically unusual - a much stronger signal than the rare hash the noise was added to avoid.

Last updated: 2026-07-30 · Facts last verified: 2026-07-30