← All articles

WebGL fingerprinting explained, mechanically

5 min read fingerprintingwebglguide

A page creates a WebGL context, asks it a series of questions about the graphics hardware underneath, and draws a small scene to measure how that hardware renders it. None of this needs your permission, because a canvas and a 3D context are ordinary web APIs. The answers vary by GPU model, driver, and operating system in ways more granular than almost anything else the browser exposes for free.

The two strings that name your hardware

The WEBGL_debug_renderer_info extension exists specifically to unmask two parameters that are otherwise generic:

const gl = document.createElement('canvas').getContext('webgl')
const info = gl.getExtension('WEBGL_debug_renderer_info')
gl.getParameter(info.UNMASKED_VENDOR_WEBGL)
gl.getParameter(info.UNMASKED_RENDERER_WEBGL)

Without the extension, gl.getParameter(gl.VENDOR) and gl.getParameter(gl.RENDERER) typically return something generic like WebKit and WebKit WebGL. With it, a real machine on Windows returns something shaped like ANGLE (NVIDIA, NVIDIA GeForce RTX <model> Direct3D11 vs_x_x ps_x_x, D3D11) or the Intel/AMD equivalent. That single string discloses the GPU vendor, the specific model, the graphics backend translating WebGL calls to native calls, and the shader model the driver reports. On macOS the backend name changes to Metal or OpenGL instead of Direct3D, and on Linux it is typically OpenGL or Mesa naming the actual card.

The parameter list that varies by hardware

Beyond the two headline strings, getParameter answers dozens of hardware-dependent questions, each contributing a bit of entropy: MAX_TEXTURE_SIZE, MAX_VERTEX_ATTRIBS, MAX_VIEWPORT_DIMS, MAX_RENDERBUFFER_SIZE, MAX_COMBINED_TEXTURE_IMAGE_UNITS, and ALIASED_LINE_WIDTH_RANGE among them. None of these are set by the browser vendor. They are read directly from the driver's reported capabilities, so a mid-range mobile GPU and a desktop card report different numbers even running the identical browser build.

Shader precision adds another layer. gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT) and its siblings for FRAGMENT_SHADER and the MEDIUM/LOW precisions return a rangeMin, rangeMax, and precision triple for each combination, and these differ across GPU families because floating point handling in shaders is itself hardware dependent.

Finally, gl.getSupportedExtensions() returns the full list of WebGL extensions the driver exposes, often forty or more entries, and the exact set, order, and naming conventions correlate tightly with GPU and driver combination.

The rendering hash

The same logic that makes canvas text rendering machine-specific applies again here, one layer deeper. A page draws a scene with specific shaders, lighting, or geometry into a WebGL context, calls gl.readPixels() to pull the rendered bytes back out, and hashes them. The chain from shader source to final pixel passes through the GPU's own rasterizer and floating-point unit, so the hash is stable across repeated draws on one machine and diverges across GPU models. See our canvas fingerprinting post for the equivalent mechanism using the 2D context; the two are frequently compared against each other, since a script claiming a specific GPU should also produce a canvas hash consistent with that GPU's known rendering behaviour.

Why a software renderer is the loudest tell

SwiftShader and llvmpipe are software implementations that render WebGL entirely on the CPU when no real GPU acceleration path is available, and their vendor and renderer strings say so explicitly. A consumer laptop or desktop reporting a software renderer is a direct contradiction: real hardware, even integrated graphics, exposes a hardware-accelerated path. A detector does not need to guess here. It reads the renderer string, matches it against a short, well-known list of software fallbacks, and treats a match as close to certain proof the browser is running in an environment with no real display adapter attached, which is exactly the profile of an automated headless worker rather than a laptop.

Why the renderer string must match the claimed operating system

The graphics backend named inside the renderer string is not decorative. ANGLE ... Direct3D11 only exists on Windows, because ANGLE translates WebGL calls to Direct3D specifically on that platform. A user agent claiming macOS paired with a Direct3D backend, or a Linux user agent paired with Metal, is not a rare coincidence: it is two subsystems each telling the truth about a different, incompatible machine, a cheaper signal to check than almost any single hardware detail alone.

Where a simpler tool is the right answer

If your goal is browsing privately rather than operating separate identities, WebGL fingerprinting is largely a solved problem already, for free. Firefox's resist-fingerprinting mode returns a fixed, generic renderer string to every site rather than your real hardware, and Tor Browser blocks WebGL entirely by default. Both are effective because they make large numbers of unrelated users report the same thing, which is a genuinely different strategy from producing one coherent-but-distinct machine per profile. Neither helps if you need several separate accounts that each need to look like their own real device, but that is a different job than the one they are built for.

Check your own values

Run this in any browser console:

function webglReport() {
  const gl = document.createElement('canvas').getContext('webgl')
  const info = gl.getExtension('WEBGL_debug_renderer_info')
  return {
    vendor: gl.getParameter(gl.VENDOR),
    renderer: gl.getParameter(gl.RENDERER),
    unmaskedVendor: info ? gl.getParameter(info.UNMASKED_VENDOR_WEBGL) : null,
    unmaskedRenderer: info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : null,
    maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
    maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
    aliasedLineWidthRange: gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
    extensionCount: gl.getSupportedExtensions().length,
  }
}
console.log(webglReport())

CreepJS scores exactly this combination of strings and limits against known-good hardware profiles, and flags a mismatch between the claimed OS and the reported graphics backend as its own category. For the full set of properties that need to agree with one another, see the fingerprint consistency checklist, and for the underlying idea across every signal in this category, our overview of browser fingerprinting.

Try it on the free tier.

Unlimited local profiles, no credit card. Check it against the detectors yourself.