Canvas fingerprinting explained, mechanically
A script creates a canvas element that never gets attached to the page, draws some text and a shape or two into it, then reads the pixels back out and hashes them. That hash is the fingerprint. Nothing is stored on your machine and nothing needs your permission, because canvas drawing and reading are ordinary web APIs every page is allowed to use.
The mechanism
The drawing step is deliberately chosen to maximise variation between machines: a specific font at a specific size, sometimes an emoji, sometimes a gradient or a rotated shape, rendered in a particular color. None of that is random. It is picked because text rendering is one of the least standardized parts of a browser.
The reading step uses one of two calls:
canvas.toDataURL()serializes the whole canvas to a base64-encoded PNG string. The page hashes the string directly, or decodes it back to pixels first.ctx.getImageData(x, y, w, h)returns the rawUint8ClampedArrayof RGBA bytes straight from the backing bitmap, skipping PNG encoding entirely. Sites frequently call both and compare, since encoding can round pixel values slightly and a mismatch between the two is its own signal.
Either way, the same underlying bitmap is the source, and that bitmap is where the entropy comes from.
Why the same draw command produces different pixels on different machines
The instruction "draw this text at this size" does not fully determine the output. Between instruction and pixel sits a chain of machine-specific decisions: which font file backs the requested family, how the OS text rasterizer hints and anti-aliases glyph edges (DirectWrite, CoreText and FreeType all disagree slightly), whether the GPU or the CPU renders the path, and color management on the way out. Change the GPU, the driver version or the OS, and the pixel bytes shift, sometimes by one bit in a handful of pixels, enough to flip the hash completely.
None of that chain changes between two calls in the same session on the same machine. Same GPU, same driver, same font file, every time. That is why the hash is stable on your machine and different on someone else's: it measures the deterministic output of one specific rendering pipeline, not anything random.
Three ways a spoofing attempt gives itself away
Blocking canvas reads outright. Some privacy tools make getImageData throw or return a blank image. Almost nobody does this by default, so a blocked canvas read is itself a rare, identifying signal, not an absence of one. A script can bucket "canvas blocked" as its own small population and treat it as suspicious on that basis alone.
Randomizing the output on every call. Tools that add noise to each toDataURL or getImageData call so the hash differs each time are solving the wrong problem. Real hardware is deterministic, so a page can call the canvas function twice in one session and compare. A mismatch confirms the tool is altering output in real time, a stronger finding than "this hash is merely uncommon." Detectors run exactly this two-read comparison.
Reusing one hash across many profiles. Faking a fixed, plausible canvas value and applying it to every profile a tool creates solves the randomization problem but creates a new one: if a script sees the same hash across thousands of sessions that otherwise look like different people, that is a cluster, and clusters are what correlation analysis is built to find. The isolation goal, each profile looking like its own machine, fails the same way whether the shared value is obviously fake or quietly identical.
What holds up
A canvas value that comes from an actual render path, differs appropriately between separate profiles, and stays exactly the same across repeated reads within one profile, session after session. That is a property of how the value is produced, not a filter applied after the fact: generated once per profile and applied in the browser engine itself, below the JavaScript layer, with no override script sitting on top of the real output for a page to catch mid-call.
Where a simpler tool is the right answer
If the goal is personal privacy rather than running many separate identities, you do not need anything that touches canvas output per session at all. Firefox's resist-fingerprinting mode puts canvas reads behind a permission prompt, free, no extension required. Tor Browser goes further and normalizes canvas output so a large fraction of its users produce the same value, a genuine defence because it works by blending you into a crowd rather than standing you apart as one more coherent but distinct machine. Neither helps if you need ten separate accounts to look like ten separate people, but that is a different problem than the one they solve.
Run the two-reads test yourself
Open your browser console on any page and run this twice, back to back:
function canvasHash() {
const c = document.createElement('canvas')
const ctx = c.getContext('2d')
ctx.textBaseline = 'top'
ctx.font = '14px Arial'
ctx.fillText('canvas fingerprint test', 2, 2)
return ctx.getImageData(0, 0, c.width, c.height).data.slice(0, 40).join(',')
}
console.log(canvasHash())
console.log(canvasHash())
On a real, unmodified browser the two lines print identically, every time. If you are testing an anti-detection extension or tool and the two lines differ, that difference is the finding: you have caught it randomizing in real time, which is more identifying than whatever value it was trying to hide.
CreepJS runs a version of this comparison automatically and flags canvas inconsistency as its own category. For the broader set of properties that need to agree with each other, see the fingerprint consistency checklist and our overview of browser fingerprinting.
Try it on the free tier.
Unlimited local profiles, no credit card. Check it against the detectors yourself.