How canvas fingerprinting works
A script adds a hidden <canvas> element (a drawing surface) to the page, draws shapes, gradients, and text on it using canvas.getContext("2d"), then calls canvas.toDataURL() to read the pixels back out. The exact pixels depend on several things: the GPU brand and model, the driver version, how the OS renders fonts (Windows ClearType vs macOS CoreText), and canvas DPI scaling (how pixels map to the display). Hashing that pixel data gives a short identifier that stays the same for one device across visits, and differs from almost every other device.
WebGL fingerprinting does the same thing but for 3D rendering, and it gives away even more: it can read the GPU name directly via gl.getParameter(gl.RENDERER). Real Chrome returns something like ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0), while headless Chrome with no GPU returns a generic software string or fails to start the WebGL context at all.
Why scrapers get caught
Headless browsers usually run on servers with no GPU, so they fall back to software rendering — either Chrome's built-in SwiftShader or Mesa llvmpipe on Linux. Both are predictable: they always produce canvas hashes from a small, known set. Anti-bot systems keep a catalogue of these hashes and flag any visitor that matches one. Worse still, if the script asks for the WebGL renderer string and gets SwiftShader Device 0x0000C0DE, that exact value is already on a blocklist.
Trying to fix this by overriding toDataURL() in JavaScript backfires. Checking Function.prototype.toString.call(canvas.toDataURL) shows the modified source code instead of the expected [native code] marker — so the patch itself becomes the giveaway that this is a bot.
How consistent canvas rendering works
Approaches that produce consistent results work deep in the browser's C++ rendering code, not in JavaScript. CloakBrowser patches Chromium's rendering layer to add a tiny, consistent amount of noise to the pixels before toDataURL() returns them — every profile gets a different hash, yet the image looks identical to the eye. Camoufox does the same for Firefox using patches applied when the browser is built. The gold standard is still real browsers on real consumer hardware: an actual laptop with an Intel integrated GPU produces a genuine canvas hash that no blocklist will match.
A 2026 development makes this messier: services like Bablosoft PerfectCanvas now sell real canvas hashes harvested from real consumer GPUs, which can be replayed into headless sessions. In response, anti-bots are combining canvas checks with signals that are harder to fake — like WASM SIMD timing (how fast the CPU runs certain math) and the physics of mouse movement (behavioural Bezier curves) — so a clean canvas hash on its own no longer proves a browser is real.
PerfectCanvas — replaying a real machine's output
The naive approach is to add random noise to the canvas pixels. It fails because anti-bot vendors expect each machine to give stable output: a hash that changes on every page load looks more bot-like than a hash that is wrong but consistent. The current best technique is canvas replay, made popular by the PerfectCanvas project that ships inside multilogin and several commercial anti-detect browsers.
It works in two steps: (1) on a real machine, run a large set of known drawing operations and save the resulting pixel data, then (2) when the headless browser is asked to draw any of those same operations, hand back the pre-recorded real pixels instead of the degraded SwiftShader output. The result is byte-for-byte identical to the real machine for any test the vendor knows to run, and it stays consistent within a session — exactly what a real device looks like.
The weak spot is unfamiliar drawing operations. A vendor that suspects replay can request a render the database has never seen — the headless browser then falls back to its real (broken) output, and the mismatch gives it away. The whole contest comes down to whether the replay database covers more operations than the vendor's tests probe.
