Orange textured background

Concept

Comparing two device fingerprints: 6 match modes and when to use each

A single hash and a string equality check are not a comparison API. Here is what six built-in modes actually give you and how to pick the right one.

Most fingerprinting libraries hand you a hash and ask you to write the comparison logic. The six built-in modes in compareFingerprints cover the full range from exact hash equality to hardware-only cross-browser matching, each with fuzzy tolerance, a numeric score, and a diff vector.

Why exact-match is not enough

The simplest version of fingerprint comparison is string equality: collect two fingerprints, compare the hash strings, done. This works when the environment is static, but real-world device environments are not static.

A user's screen resolution changes when they connect an external monitor. Audio DSP output drifts by fractions of a percent across OS updates. A time zone changes when someone travels. In most fingerprinting workloads, the question is not 'are these two fingerprints bit-for-bit identical?' but 'are these two fingerprints similar enough to conclude they came from the same device?'

The second question requires a comparison API with nuance: per-signal fuzzy matching, a similarity score, and a way to restrict the comparison to only the signal categories relevant to the question being asked. ThumbmarkJS and FingerprintJS hand you a hash and make you build that. `compareFingerprints` ships it.

The six modes and the use case each serves

Each mode is a preset that determines which signals are included in the comparison and how strictly they are scored. Fuzzy matching is enabled by default in all modes except `strict`.

The mode names map directly to the `mode` option in `ComparisonOptions`. You pass one when calling `compareFingerprints(fpA, fpB, { mode: 'cross-browser' })`.

The six comparison modes

ModeSignal setFuzzy matchingBest for
exactAll signals from both fingerprintsEnabledDefault. All-signals match with graceful tolerance for minor drift
cross-browserHardware-bound signals onlyEnabledMatching the same device across Chrome, Safari, Firefox, Brave
hardware-onlyHardware-bound signals onlyEnabledExplicit hardware filter when semantic intent differs from cross-browser
engine-onlyEngine-bound signals onlyEnabledDetecting that the browser changed on the same device
strictAll signalsDisabled, hash equality onlyZero tolerance: any signal change fails the match
lenientAll signalsEnabledMaximum tolerance for expected signal drift over long time windows

Fuzzy matching: what it means and why it matters

Fuzzy matching means that for signals with dedicated fuzzy functions, a partial match produces a score between 0.0 and 1.0 rather than snapping to 0 on any difference. Five signals have fuzzy functions: screen dimensions (tolerating up to a 10% resolution change from display scaling or monitor reconnect), platform properties (OS must match exactly, but hardware specs contribute partial credit), timezone, media device counts, and audio waveform values.

For all other signals, a hash mismatch scores 0.0: there is no fuzzy path for signals without a dedicated fuzzy function. In `strict` mode or when `fuzzyMatching: false` is set explicitly, the fuzzy dispatch is bypassed entirely and every signal scores either 1.0 or 0.0.

The practical result is that a returning user who connected an external monitor since their last session still scores a high `matchScore` in `exact` mode, whereas a strict equality check would fail them.

import { getFingerprint, compareFingerprints } from 'doorman-benny';

// Collect two fingerprints: one stored previously, one just collected.
const storedFp = await getFingerprint(); // saved previously
const currentFp = await getFingerprint(); // just collected

// Default comparison: all signals, fuzzy matching, default threshold.
const exact = compareFingerprints(storedFp, currentFp);
console.log(exact.matchScore);     // 0 to 100
console.log(exact.match);          // true if matchScore meets the threshold
console.log(exact.hardwareMatch);  // string equality of hardwareFingerprint fields
console.log(exact.similarity);     // weighted 0.0 to 1.0
console.log(exact.diffVector);     // 26-element float array for ML use

// Cross-browser mode: hardware signals only.
// Use this when the question is 'same device, regardless of browser'.
const crossBrowser = compareFingerprints(storedFp, currentFp, {
  mode: 'cross-browser',
});
console.log(crossBrowser.hardwareMatch);
console.log(crossBrowser.matchScore);

// Strict mode: no fuzzy tolerance.
const strict = compareFingerprints(storedFp, currentFp, { mode: 'strict' });

// Custom threshold.
const custom = compareFingerprints(storedFp, currentFp, {
  matchThreshold: 90,
  excludeSignals: ['speech_voices', 'storage_quota'],
});

compareFingerprints is synchronous. Both inputs must be fully collected FingerprintResult objects before the call.

Picking a mode per scenario

The right mode depends on the question being asked and the tolerance for false positives and false negatives in your application.

For trial-abuse prevention, `cross-browser` mode with `hardwareMatch` as the primary gate is the right starting point. The abuser changes browsers to evade a per-browser fingerprint, so restricting the comparison to hardware-bound signals is exactly the correct filter. A `hardwareMatch: true` verdict means the hardware identity is identical regardless of which browser was used.

For detecting returning visitors in the same browser, `exact` mode with fuzzy matching captures the legitimate drift (minor screen resolution changes, audio drift across OS updates) without requiring perfect bit-for-bit equality.

For account takeover detection, where you want to know if the session is running in an unexpected browser on the same device, `engine-only` mode compares exclusively the engine-bound signals. A low score there while `hardwareMatch` is still true means: same device, different browser, which is a meaningful anomaly signal for sensitive actions.

For the most permissive matching, such as re-identifying users across long time windows where significant drift is expected, `lenient` mode maximises fuzzy tolerance. Use this when you prefer false negatives over false positives.

For cryptographic-style verification where any change at all should fail the match, use `strict` mode. This is appropriate for device-bound key scenarios, not for ordinary re-identification.

Mode selection guide by scenario

ScenarioRecommended modeKey signal to check
Block free-trial re-signup after browser switchcross-browserhardwareMatch
Detect duplicate signups / multi-accountingcross-browserhardwareMatch or match
Recognize returning visitor, same browserexactmatch + matchScore
Detect browser change on same device (ATO hint)engine-onlymatch (low score = browser changed)
Re-identify user after long absence with expected driftlenientmatch
Device-bound verification (zero tolerance)strictmatch
Custom signal subset (omit noisy signals)exact with excludeSignalsmatch + matchScore

The diff vector and downstream ML use

Every `compareFingerprints` call returns a `diffVector`: a fixed 26-element float array where each element is the per-signal match score (0.0 to 1.0) in the fusion order. Signals not included in the comparison mode are set to 0.0.

The diff vector is designed as a feature input for ML pipelines. If you are training a classifier to detect multi-accounting or fraud, the diff vector gives you a consistent feature representation of the signal-level divergence between two fingerprints. The vector dimensionality is fixed across library versions, so a model trained on one version's output is compatible with future versions.

For most anti-fraud applications you do not need to build an ML model. The `matchScore`, `hardwareMatch`, and `match` fields are sufficient. The diff vector is available for teams that want to go deeper.

Frequently asked questions

Can I compare a fingerprint from an older library version with a newer one?

Yes, with a caveat. If the signal set changed between versions (for example, new signals were added), the diffVector will have zeros for signals that were absent in the older fingerprint, and those signals will not contribute to the similarity score. The comparison still works but may produce a lower matchScore than a same-version comparison. The crossBrowser.stableSignals and crossBrowser.unstableSignals fields help diagnose which signals contributed.

What does a matchScore of 0 mean?

A matchScore of 0 most often means a hard constraint violation fired before any signal scoring ran. The os_changed constraint fires when the normalised platform string differs between the two fingerprints, and causes an early return with matchScore: 0. Check constraintViolations on the result to see what fired. A genuinely zero similarity score without a constraint violation is unusual and may indicate two very different devices or an empty signal set.

Why does hardwareMatch sometimes disagree with match?

hardwareMatch is simple string equality of the two hardwareFingerprint fields. match is a weighted similarity verdict that considers all signals in the mode's signal set. They can disagree in either direction: hardwareMatch can be true while match is false (hardware identical, but engine signals drifted past the threshold), or match can be true while hardwareMatch is false (high overall similarity, but the hardware hash is slightly different due to minor drift). Treat them as independent signals answering different questions.

How is this different from what ThumbmarkJS or FingerprintJS offer?

ThumbmarkJS and FingerprintJS return a hash. Comparison logic, fuzzy matching, score calibration, and the hardware-vs-engine split are all left to the caller. compareFingerprints is the built-in answer: six modes, per-signal fuzzy functions, a numeric score, a diff vector, and a hardware hash that is cross-browser stable by construction. It is part of the free tier.

Is the diff vector stable across library versions?

The vector has a fixed length (26 elements, one per signal in the fusion order). Signal positions within the vector are stable once a signal is added to the library. New signals are appended at the end, and old positions do not shift. A model trained on the diff vector from one version will remain compatible with future versions, with zeros for any signals that did not exist in the older fingerprint.

Get started

Stop building comparison logic from scratch

Six comparison modes, per-signal fuzzy matching, and a 26-element diff vector ship in the free tier. Read the API reference or install and start comparing.

Get startedcompareFingerprints API reference

Related posts

Last reviewed June 9, 2026