All articles Fundamentals

WebGL Fingerprinting: What Your GPU Reveals

Every browser that supports WebGL can be asked to render a small scene and report back details about the graphics stack that produced it. That reply depends on the GPU model, the driver version, the operating system compositor, and the browser’s own rendering choices. Taken together, those details form one of the highest-entropy signals available to a client-side fingerprinting script.

WebGL fingerprinting matters because it survives many of the countermeasures that defeat cookies. It works in private windows, it does not require storage permissions, and it produces a value that stays remarkably stable across sessions on the same physical machine. This guide explains what the GPU actually reveals, how a robust fingerprint is computed, and where the technique breaks down.

What WebGL exposes to a script

A WebGL context answers two broad classes of query: static parameter strings and dynamic rendering output. Both contribute entropy, and they fail differently under spoofing.

The static parameters are read directly from the driver:

  • RENDERER and VENDOR (and their unmasked variants via the WEBGL_debug_renderer_info extension) — for example, a string naming an Apple M-series GPU or an ANGLE-wrapped Direct3D backend.
  • Supported extensions, returned as a list whose order and membership vary by GPU family.
  • Numeric limits: maximum texture size, maximum vertex attributes, viewport dimensions, and dozens of shader precision ranges.
  • Aliased and antialiased line width ranges, point size ranges, and combined texture image units.

The dynamic output is subtler. A script renders a known scene — often a gradient, a rotated model, or text with specific blending — then reads the pixels back with readPixels and hashes them. Because floating-point rounding, antialiasing, and texture sampling differ across GPUs and drivers, two devices with identical parameter strings can still produce distinct pixel hashes.

Why the GPU is so identifying

The combination of static and dynamic signals is powerful because it reflects a physical stack, not a configuration a user picked. As we discuss in browser fingerprinting entropy explained, entropy is only useful when it is both high and stable, and WebGL scores well on both.

Consider a simplified breakdown of where the bits come from:

SourceTypical stabilityRelative entropy
Unmasked renderer stringVery highHigh
Shader precision rangesVery highMedium
Extension list and orderHighMedium
Rendered pixel hashHighHigh
Numeric limitsVery highLow

A single value like the renderer string might be shared by millions of identical laptops. But once you combine it with the pixel hash, the extension ordering, and the audio and font signals covered in audio fingerprinting explained and font fingerprinting: how it works, the joint distribution narrows sharply.

Computing a stable WebGL fingerprint

Naive implementations hash everything they can read, which produces a value that changes whenever a driver updates or the browser toggles a rendering flag. A durable fingerprint separates the volatile parts from the stable ones and hashes them into distinct components.

function webglFingerprint() {
  const canvas = document.createElement('canvas');
  const gl = canvas.getContext('webgl') ||
             canvas.getContext('experimental-webgl');
  if (!gl) return { supported: false };

  const dbg = gl.getExtension('WEBGL_debug_renderer_info');
  const stable = {
    vendor: dbg && gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL),
    renderer: dbg && gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL),
    maxTexture: gl.getParameter(gl.MAX_TEXTURE_SIZE),
    extensions: (gl.getSupportedExtensions() || []).sort(),
  };

  // Dynamic component: render and read back.
  const render = renderSceneAndHash(gl);

  return { supported: true, stable, render };
}

Keeping the extension list sorted removes ordering noise you do not want, but note that unsorted ordering is itself a signal — you may choose to hash both. The rendered component should use a fixed scene and integer-friendly geometry to reduce cross-run jitter on the same device. For a broader treatment of the rendering-and-readback pattern, see how canvas fingerprinting works, which shares the same underlying mechanics.

Spoofing, noise, and anti-detect browsers

WebGL is a favorite target for evasion because a single injected shim can rewrite every value. Anti-detect browsers and privacy extensions take a few approaches:

  • Static substitution: return a plausible renderer string from a different device. This is easy to detect when the substituted string is inconsistent with the numeric limits or the pixel hash it should imply.
  • Per-pixel noise: perturb readPixels output so the hash changes every read. This defeats naive hashing but is detectable because a genuine GPU produces identical output on repeated identical renders.
  • Full context blocking: return null for getContext('webgl'). Rare enough that its absence is a signal in itself.

The tell is internal consistency. A real Apple GPU implies a specific set of supported extensions, precision ranges, and antialiasing behavior. When a script cross-checks the renderer string against those implied properties and finds a mismatch, the device looks tampered rather than anonymous. That contradiction feeds directly into a suspect score, and we cover the general pattern in detecting canvas spoofing and detecting antidetect browsers.

Using WebGL responsibly in a fingerprint

WebGL should never be your only identifier. Browsers increasingly restrict the unmasked renderer, virtualized and cloud environments share GPUs across many tenants, and headless rendering can produce software-rasterized output that collides across unrelated machines. Treat WebGL as a strong contributor to a composite ID, weighted by its own confidence.

Practical guidance:

  • Hash static and dynamic components separately so a driver update degrades only one part.
  • Record whether the unmasked extension was available; its absence changes how much weight the signal deserves.
  • Detect software rendering (for example, an ANGLE/SwiftShader backend) and down-weight it, since it collides heavily in datacenters.
  • Feed contradictions, not just values, into your model. A missing or inconsistent GPU story is more interesting than any single string.

When these signals roll up into a confidence score alongside network and behavioral data, WebGL earns its place. You can watch the raw values change across browsers and spoofing tools in the playground, and the device fingerprinting overview shows where it sits in the wider pipeline.

Frequently asked questions

Does WebGL fingerprinting work in every browser?

Most modern browsers expose WebGL, but the amount of detail varies. Some browsers restrict the renderer string or add noise, so WebGL is best used as one signal among many rather than a standalone identifier.

Can users block WebGL fingerprinting?

They can disable WebGL, use anti-detect browsers, or install extensions that randomize outputs. Each of those choices is itself observable and often raises a device’s suspect score rather than lowering it.

WebGL turns a request for graphics into a description of the hardware and drivers behind the screen. That description is rich, stable, and hard to fake convincingly, which is exactly why it belongs in a layered fingerprint rather than at the center of one. Combine it with network and behavioral signals, watch for internal contradictions, and let the GPU tell you when a device is being something other than what it claims.

Run it yourself

Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.

Keep reading