Stop Relying on Cookies: A 15-Minute Practical Guide to Browser Fingerprinting
Learn how to replace traditional cookie tracking with stable browser fingerprinting. This guide covers FingerprintJS installation, generating consistent visitor IDs (even in incognito mode), leveraging confidence scores, and implementing a practical anti-duplicate form submission feature for real-world web apps.

Stop Relying on Cookies: A 15-Minute Practical Guide to Browser Fingerprinting
Hi everyone.
Recently, I helped an e-commerce friend troubleshoot a "brush order" (fake transaction) problem. His first instinct was: "Can't we just use cookies to mark users?" The reality hit quickly: users open incognito windows, clear browser data, and the cookies vanish instantly. The same device in a new window looks like a "new user". If you've ever dealt with user deduplication, anti-fraud, or malicious request detection, this tutorial is for you.
By the end of this guide, you will:
- Install and initialize FingerprintJS in your project.
- Obtain a stable visitor fingerprint ID (consistent even in incognito mode).
- Leverage confidence scores to detect device/browser switches.
- Build a practical "duplicate form submission prevention" case ready for your production code.
Let's dive straight into the code.
1. Prerequisites
- Node.js ≥ 16 (if using npm); pure CDN usage requires no local setup.
- Basic Frontend Knowledge: Familiarity with
async/awaitand npm package management. - Project Environment: We'll cover both a plain HTML + JS setup and an npm-based project (React, Vue, Next.js, etc.).
FingerprintJS is currently one of the most starred and mature open-source browser fingerprinting libraries on GitHub. It calculates a hashed visitor ID by collecting browser attributes (Canvas, WebGL, fonts, timezone, etc.). The key takeaway: this ID does not rely on Cookies or LocalStorage. Even if users clear their data or switch to incognito mode, the same device will generate a consistent fingerprint.
2. Quick Start: Getting the Visitor ID in Two Steps
Step 1: Install Dependencies
If you're working within an npm/yarn ecosystem, run the following in your project root:
bash
npm install @fingerprintjs/fingerprintjs
Why npm over CDN? While CDN is convenient, browsers like Brave or ad-block extensions often block CDN domains. Installing via npm keeps the library local, ensuring higher stability.
Step 2: Initialize and Fetch the Fingerprint
Here are the core three steps. I recommend placing this in your app's startup logic:
javascript
import FingerprintJS from '@fingerprintjs/fingerprintjs';
// ① Load the Agent during startup (call only once, returns a Promise)
const fpPromise = FingerprintJS.load();
// ② Call get() where you need user identification
(async () => {
const fp = await fpPromise; // Get the instance
const result = await fp.get(); // Collect fingerprint
console.log('Visitor ID:', result.visitorId);
console.log('Confidence:', result.confidence.score); // Score between 0 and 1
})();
Key breakdown:
load(): Initializes the collection engine and browser probes. Call it early to avoid cold-start latency when you actually need the ID.get(): Triggers the actual fingerprint collection. Returns aResultobject containing thevisitorId(core identifier) andconfidence.confidence.score: A score closer to 1 means a more stable, reliable fingerprint. If it drops below0.7, the user might be in a VM or strict privacy mode. You'll need fallback strategies.
CDN Alternative (For Plain HTML Projects)
If your project lacks a bundler, you can embed it directly via <script>:
html
<script>
const fpPromise = import('https://openfpcdn.io/fingerprintjs/v5')
.then(FingerprintJS => FingerprintJS.load());
(async () => {
const fp = await fpPromise;
const result = await fp.get();
console.log(result.visitorId);
})();
</script>
⚠️ Note: As mentioned, ad-blockers may intercept CDN links. For production, implement a fallback: use
try/catchto handle loading failures and degrade gracefully to server-side IP + User-Agent validation.
3. Practical Case: Preventing Duplicate Form Submissions
Getting the ID is just the beginning. Let's put it to work. Below is a frontend example for "preventing duplicate registrations from the same device". You can easily adapt this logic to any anti-brush scenario.
javascript
import FingerprintJS from '@fingerprintjs/fingerprintjs';
class AntiDoubleSubmit {
constructor() {
this.submittedFingerprints = new Set();
}
async initFingerprint() {
// ① Initialize only once
this.fp = await FingerprintJS.load();
}
async checkAndSubmit(formEl) {
if (!this.fp) await this.initFingerprint();
const result = await this.fp.get();
const { visitorId, confidence } = result;
// ② Bypass frontend check if confidence is too low (avoid false positives)
if (confidence.score < 0.6) {
console.warn(`Low fingerprint confidence: ${confidence.score}. Relying solely on server validation.`);
return this.doServerSubmit(formEl);
}
// ③ Check against local record
if (this.submittedFingerprints.has(visitorId)) {
alert('Please do not submit repeatedly!');
return;
}
// ④ Record fingerprint and proceed with submission
this.submittedFingerprints.add(visitorId);
return this.doServerSubmit(formEl);
}
async doServerSubmit(formEl) {
const formData = new FormData(formEl);
// ⑤ Pass visitorId to the backend for persistent storage and cross-session deduplication
formData.append('deviceFingerprint', await this.fp.get().then(r => r.visitorId));
const res = await fetch('/api/register', { method: 'POST', body: formData });
const data = await res.json();
console.log('Registration result:', data);
}
}
// Usage
const antiSubmit = new AntiDoubleSubmit();
document.getElementById('registerForm').addEventListener('submit', (e) => {
e.preventDefault();
antiSubmit.checkAndSubmit(e.target);
});
What this code achieves:
- Wraps logic in a class to avoid global variable pollution.
- Initializes the fingerprint agent only once and reuses it.
- Checks the confidence score. If too low, it skips frontend blocking and relies purely on backend validation.
- Uses a
Setto track submitted fingerprints, directly blocking repeated submissions from the same browser. - Crucial Step: Sends the
visitorIdto the backend, enabling persistent storage and cross-session deduplication.
💡 Why frontend-only anti-brush isn't enough? Client-side logic can be bypassed. Always pair frontend experience optimization with strict backend validation. Combine
visitorIdwith your risk control strategies (e.g., limiting registrations per ID per day).
4. Common Pitfalls & Best Practices
- Incognito Mode ≠ Different Fingerprint: This is a feature, not a bug. If you specifically need to distinguish "normal" vs "incognito" mode, fingerprinting isn't the right tool (and shouldn't be used for that).
- VMs & Automated Testing: Headless Chrome or CI environments generate fewer signals, often resulting in lower
confidence.score. Set separate thresholds for these environments. - Privacy Compliance (GDPR): If you operate in Europe, pay strict attention to GDPR. Browser fingerprints are considered "personal identifiable information". You must declare the purpose in your privacy policy and provide an opt-out mechanism.
- Open Source vs Commercial: The open-source version computes everything client-side, with accuracy limited by available client signals. For high-stakes needs like financial anti-fraud, the official Fingerprint Identification (hybrid client+server) is recommended. However, the open-source version is more than sufficient for most anti-spam/deduplication scenarios.
5. Summary
In under 15 minutes, we've accomplished:
- ✅ Installed FingerprintJS and obtained a stable visitor fingerprint ID.
- ✅ Understood the
confidencescore and how to set business-appropriate thresholds. - ✅ Built a production-ready "duplicate submission prevention" example applicable to registrations, voting, or flash sales.
Next Steps:
- Store the
visitorIdin your user database. Combine it with IP addresses and registration timestamps for aggregation analysis. You'll quickly spot "new accounts" that are actually old devices in disguise. - Check out the API Reference in the FingerprintJS repository to learn how to customize collection components.
- If you're using React/Vue, refer to the official StackBlitz demos for framework-specific implementations.
Have questions about integration? Drop a comment below, and I'll get back to you. Writing clean code is good, but applying it smartly is better. See you in the next post!