Device fingerprinting is the practice of reading a few dozen properties of a browser, hashing them, and treating the result as an identifier for that machine. Nobody is proud of it. It is also, for a small software business under attack, the cheapest signal an attacker cannot rotate for free. This post covers how it works, where it fails, what the law says, and when you should bother.
How browser fingerprinting works: the signals and the hash
A fingerprinting script runs in the page and asks the browser questions. Most of the answers are boring on their own. The user agent, screen.width and screen.height, the timezone from Intl.DateTimeFormat().resolvedOptions().timeZone, navigator.language, navigator.hardwareConcurrency, navigator.deviceMemory, touch support, the list of installed fonts probed by measuring text width. Each one narrows the population a little.
The interesting components are rendering side effects. A canvas fingerprint draws text and shapes to an offscreen <canvas>, reads the pixels back with toDataURL(), and hashes them. Anti-aliasing, font hinting, and GPU drivers differ between machines, so the same drawing produces different bytes. WebGL does the same with a 3D scene and adds the renderer string (UNMASKED_RENDERER_WEBGL), which names the GPU. An audio fingerprint pushes a signal through an OfflineAudioContext and hashes the output buffer, which varies with the audio stack.
The open-source FingerprintJS library, the reference point most developers start from, concatenates fifty-odd such attributes and passes them through MurmurHash3 to produce a 32-character hex identifier, as described in this walkthrough of its internals. The library was MIT-licensed through version 3, moved to the Business Source License at version 4 (non-production use only, commercial license required for SaaS), and returned to MIT at version 5 in October 2025. Read the license of whichever version you pin.
That hash is the whole product on the client side. Everything else, the "server-side fusion" the commercial vendors talk about, means keeping the raw components rather than one hash, so that a fingerprint where three components changed can still be matched to the one seen last week. The FingerprintJS README says plainly that because the open-source library generates fingerprints in the browser itself, "the accuracy is significantly lower than in the commercial version." A single hash is brittle. A component vector with fuzzy matching is not.
Accuracy, stability, and what privacy tooling does to it
Two numbers frame the whole field, and they disagree.
The EFF's 2010 Panopticlick study, How Unique Is Your Web Browser?, collected fingerprints from 470,161 browsers and found 83.6% of them unique, with the distribution carrying at least 18.1 bits of entropy. That number launched the industry. Eight years later, Hiding in the Crowd measured 2,067,942 fingerprints on a top French website and found only 33.6% unique, dropping to 18.5% on mobile. The difference is population. The EFF's sample was people who sought out a privacy test site and have odd setups; the general public buys the same three phones.
For fraud, uniqueness is the wrong question anyway. You do not need to distinguish one visitor from every human on earth. You need to notice that the same machine created its ninth account this morning, and for that a fingerprint shared by 1 in 5,000 browsers is fine, as long as it is stable across those nine attempts.
Stability is where the browsers are fighting you, and each one fights differently.
Brave randomizes. Its "farbling" perturbs canvas, WebGL, audio, plugin lists and the user agent using a random token created at browser start and mixed with each first-party domain: a site sees the same values within a session, a different site sees different values, and the same site sees new values next session. Your hash of a Brave user changes every time they restart the browser.
Safari does the same in spirit. WebKit's Private Browsing 2.0 post describes applying "tiny amounts of noise" to painted canvas pixels and to each WebAudio sample, and fixing the reported screen size to the window's innerWidth and innerHeight so screen dimensions stop leaking. The stated principle is to "make the fingerprint unique per website, and generate a new unique fingerprint for every fresh start." Those protections are on in Private Browsing and are an opt-in setting for regular browsing, so today you meet them from a minority of Safari users, and the share is Apple's to change.
Firefox goes the other way and standardizes. With resistFingerprinting enabled, canvas image extraction is blocked, the timezone is spoofed to UTC, the locale to en-US, and window dimensions are rounded to a multiple of 200 by 100 pixels. Tor Browser, built on Firefox, pushes this to its logical end: every user reports the same OS version, canvas extraction is blocked, and letterboxing keeps the content window on that 200 by 100 grid so screen size groups users into a handful of buckets.
The practical result is that a raw client hash is stable on Chrome and the Chromium browsers that ship no fingerprint defenses, and unreliable on the rest. An attacker who wants a fresh fingerprint per attempt does not need any of these browsers. Anti-detect browsers sell fingerprint profiles by the hundred, and headless Chrome with a spoofed canvas is a weekend project. Fingerprinting raises the cost of rotation from zero to something. It does not make rotation impossible.
Privacy and the law
In the EU, fingerprinting for fraud prevention sits in a gray zone that has narrowed over ten years, and you should assume consent-adjacent rules apply.
The Article 29 Working Party's Opinion 9/2014 settled the first question. Its stated key message is "that Article 5(3) of the ePrivacy Directive is applicable to device fingerprinting," because reading device characteristics counts as gaining access to information already stored in the terminal equipment, even when nothing is written. The EDPB's Guidelines 2/2023, finalized in 2024, reaffirmed that reading and extended it to newer tracking techniques.
The exemption that matters is the one for access "strictly necessary in order for the provider of an information society service explicitly requested by the subscriber or user to provide the service." Regulators have historically read it narrowly, and the 2014 opinion does not list fraud prevention among its examples. A fraud check at checkout has a stronger argument than a fraud check that runs on every page view of a marketing site, and a fingerprint hashed at collection and used only for security has a stronger argument than one shared with an ad network. Nobody can promise you the exemption. What you can do is scope the collection to the moments where abuse happens, disclose it in the privacy policy, hash on the server, and set a retention window.
Under GDPR proper, the hash is personal data because it singles out a device, so it needs a lawful basis. Legitimate interest for fraud prevention is the usual one, and Recital 47 names fraud prevention as an example of a legitimate interest. In California, the CCPA's definition of personal information explicitly includes device identifiers and probabilistic identifiers, so the fingerprint belongs in your notice at collection and your deletion pipeline.
None of this is a reason to skip fingerprinting at signup or checkout. It is a reason to do it in one place, for one purpose, with a hash you can delete.
Why the device is the key that matters
Every abuse defense needs an identity key: something the attacker must spend money to change. Compare the candidates.
IP address is the key everyone reaches for first, and it fails in both directions. Carrier-grade NAT puts an entire mobile network behind a few thousand addresses, so a per-IP counter blocks a university or a phone carrier. Meanwhile residential proxies rent a fresh home IP per request, so an attacker's counter never reaches two. Stripe's own card testing guidance says that "simple firewall rules or filters based on a single heuristic such as IP addresses are usually not sufficient." When one Hacker News poster asked how to stop card testing on a Stripe account, the replies that worked combined several things: tinyprojects rolled "IP-based bans on creating checkout links + notifications if a purchase has many failed attempts using diff cards," and a_simm found that "cycling (all) the keys stopped it immediately."
Email is worse. A plus-address or a disposable domain costs nothing. Gmail dots are free. Even a paid catch-all domain gives an attacker unlimited addresses for the price of one registration.
Card fingerprint is a real key at checkout, and a good one. The processor computes it from the PAN, so it survives across your accounts and across attackers who share a dump. It is useless before a card is entered, which is to say at signup and trial start, and a card tester by definition has thousands of them.
That leaves the device. A machine running an attack has one GPU, one font set, one audio stack. Changing them means a new profile, a new VM, or an anti-detect browser, each of which costs time or money per identity. It is the only key that exists at every one of the five moments you would check: signup, trial start, trial conversion, checkout, login.
For card testing specifically, the device is the only key that can carry a block at ordinary amounts. Stripe reported that at the peak of one 2022 wave Radar was blocking more than 20 million card testing attempts per day, and the individual attempts look like nothing: a $1 charge from a real-looking email at a plausible hour. A single $1 attempt is not blockable on its own merits. The eleventh $1 attempt from the same device with the eleventh distinct card is. Without a device key you cannot count to eleven, so the best you can do is rate-limit the IP and hope the proxy pool is small.
Fingerprints should be tokens
Almost every home-built integration makes the same mistake: the page computes a hash, sends it to your server in the request body, and the server trusts it.
The client is the attacker's machine. A hash computed there is a string the attacker can set to anything, and rotating it is a one-line change to the script that is already automating the checkout. Worse, a stable hash of stable components is a persistent identifier your server is storing in the clear, which is exactly the thing the privacy regulators are worried about.
The correct shape is a token exchange. The browser collects raw components and posts them, with a publishable key, to a fingerprinting endpoint that is not your application server. That endpoint fuses the components, hashes them with a server-side secret, and returns a signed, short-lived token. Your page forwards the token with the checkout request. Your server passes it through to the verdict call, where the signature is checked and the token is resolved back to a device identity that never left the server.
This does three things at once. The attacker cannot forge a device identity, because forging a token requires the signing key. The attacker cannot replay a token indefinitely, because it expires. And your application never handles a raw device identifier, which keeps the fingerprint out of your database, your logs, and your data subject access requests.
This is the shape I built. The @portreeve/browser snippet collects components with a publishable pk_ key and exchanges them server-side for a signed, short-lived device token; all fusion and hashing happens on Portreeve's servers, identity keys are hashed from day one, and raw payloads are scrubbed to one-way hashes after 90 days. Two of the card-testing rules that return block rather than review read a device-keyed counter, so without the snippet those shapes top out at review. The device fingerprinting docs cover the snippet, and the reason codes reference lists which codes depend on it.
Whatever you use, the rule is the same. Treat the device identity as something your server resolves, never something your client asserts.
Build, buy, or skip it
The decision is mostly about what the attacker earns per attempt.
Skip it for a free tool with no per-user cost: a formatter, a calculator, a static docs site with a newsletter box. There is nothing to farm. Fingerprinting there is pure privacy cost with no security benefit, and it is the case where the ePrivacy exemption argument is weakest.
Skip it, for now, if you sell one product at one price and have never seen an attack. Watch the Stripe dashboard for the 402 spike. When it comes, you will have a week to add the snippet.
You need it for anything where an account is worth money to create. Card testing at any checkout that accepts cards, because the attacker's whole business is rotating cards from one machine. Free trials that hand out compute, API credits, or LLM tokens, where each new account is a fresh allocation. Referral and credit programs, where the payout is direct. Login, where the question is "is this the device this account normally uses" and the answer is unavailable without a device key. The trial and credit cases are the ones where IP and email fail hardest, because the attacker is not rushing; twenty signups a day from twenty residential IPs never trips a velocity counter that has no device to count on.
Build it if you want to. FingerprintJS 5 is MIT, the components are documented, and a token-exchange endpoint is an afternoon. The hidden cost is the matching layer: the fuzzy comparison across component drift, the handling of Brave and Safari noise, the graph that links this device to the accounts it touched and the cards those accounts probed. That is the part that turns a hash into a signal, and it is the part that gets rebuilt every time a browser ships a new defense. Budget for maintenance, not for the first version.
Buy it if you would rather spend the afternoon on the product. Fingerprint's commercial tier is $99 per month for 20,000 API calls with $4 per additional thousand, and it gives you a stable visitor ID and nothing else. You still have to build the counters, the linking, and the verdict logic on top, or pick a tool that ships the device key already wired to the card, email, and cluster signals. Either way, the fingerprint is an input to a decision. Nobody should be shipping the hash straight into an if statement.
If you are being card tested this week, the card testing post covers how the probes work and what they cost you. For the longer fix, the free tier screens 1,000 events a month with no card required. Create an account, add the browser snippet, and the next $1 ladder from one machine gets counted.