cdpfleet Docs Sign up free

Examples & benchmarks / #52 · 2026-10-23 · Benchmark · Medium · for web scraping

Installed fonts as a fingerprint: what a width probe sees on each Camoufox persona

The classic font probe — 46 families measured against the generic fonts — on Camoufox's Windows, macOS and Linux personas, with extra `fonts` and with `custom_fonts_only`. The list follows the OS and repeats from launch to launch; `fonts` adds families to the persona's list (again since 2026-10-30; from 2026-10-22 it replaced the list).

Camoufox

Run on production on 2026-10-30: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

A benchmark: the results below are what this setup measured on the dates shown, on our own test runs. Sites, networks and browsers change, so rerun the code before you rely on the numbers.

The problem

Which fonts a machine has is one of the oldest fingerprinting signals: a script sets a test string in each candidate family and measures it. A Windows user agent with macOS fonts, or a machine with three fonts in total, stands out. Camoufox builds the font list from its OS persona and lets you add to it or replace it. What does a probe actually see, is it the same every launch, and what do fonts and custom_fonts_only change?

What we used, and why

WhatWhy
A width probe over 46 familiesEach family is set with monospace, serif and sans-serif as fallback; if any measurement differs from the generic family alone, the family counts as installed. This is how common fingerprinting libraries do it.
os: "windows" | "macos" | "linux"The persona decides which families the page can see.
fonts: [...]Families to add to the persona's list.
custom_fonts_only: trueExpose only the families in fonts.
A hash of the detected listOne value per launch that a site could store and compare.

How it works

  1. Launch Camoufox six times one after another: Windows twice, macOS, Linux, Windows with three extra fonts, and Windows with a custom list only.
  2. In each, run the width probe over the same 46 candidates and record the detected families, their hash, and whether the three generic families measure the same.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { createHash } from 'node:crypto';
import { firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// The families a font-fingerprinting script typically tries: Windows, macOS, Linux and popular extras.
const CANDIDATES = [
  'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Candara', 'Comic Sans MS', 'Consolas', 'Constantia', 'Corbel', 'Courier New',
  'Georgia', 'Impact', 'Lucida Console', 'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype', 'Segoe UI', 'Tahoma',
  'Times New Roman', 'Trebuchet MS', 'Verdana', 'American Typewriter', 'Avenir', 'Avenir Next', 'Futura', 'Geneva', 'Gill Sans',
  'Helvetica', 'Helvetica Neue', 'Lucida Grande', 'Menlo', 'Monaco', 'Optima', 'SF Pro', 'DejaVu Sans', 'DejaVu Serif',
  'Liberation Sans', 'Liberation Mono', 'Noto Sans', 'Ubuntu', 'Cantarell', 'Roboto', 'Open Sans', 'Fira Code', 'JetBrains Mono',
  'Source Code Pro',
];

// The classic width probe: a family is "installed" if text set in it, with a generic fallback,
// measures differently from the generic family alone.
const PROBE = `(fonts) => {
  const span = document.createElement('span');
  span.textContent = 'mmmmmmmmmmlli10OWQ@#';
  span.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
  document.body.appendChild(span);
  const size = (family) => { span.style.fontFamily = family; return span.offsetWidth + 'x' + span.offsetHeight; };
  const generics = ['monospace', 'serif', 'sans-serif'];
  const base = {};
  for (const g of generics) base[g] = size(g);
  const found = fonts.filter((f) => generics.some((g) => size('"' + f + '", ' + g) !== base[g]));
  span.remove();
  return { platform: navigator.platform, found, generic_sizes: generics.map((g) => base[g]) };
}`;

const SETUPS = [
  { setup: 'os: windows (launch 1)', body: { os: 'windows' } },
  { setup: 'os: windows (launch 2)', body: { os: 'windows' } },
  { setup: 'os: macos', body: { os: 'macos' } },
  { setup: 'os: linux', body: { os: 'linux' } },
  { setup: 'windows + fonts', body: { os: 'windows', fonts: ['Menlo', 'Helvetica Neue', 'Noto Sans'] } },
  { setup: 'windows + fonts + custom_fonts_only', body: { os: 'windows', fonts: ['Calibri', 'Consolas', 'Segoe UI', 'Noto Sans'], custom_fonts_only: true } },
];

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

// Proxies drop a tunnel now and then: up to two retries.
async function go(page, url) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await page.goto(url, { timeout: 60000 });
    } catch (err) {
      if (attempt === 3) throw err;
      await new Promise((r) => setTimeout(r, 2000));
    }
  }
}

const results = [];
for (const s of SETUPS) {
  const session = await launch({ ...s.body, proxy: process.env.PROXY_URL, headless: true, record: false });
  const browser = await firefox.connect(session.wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const page = await (await browser.newContext()).newPage();
    await go(page, 'https://httpbin.org/html');
    const r = await page.evaluate(`(${PROBE})(${JSON.stringify(CANDIDATES)})`);
    results.push({
      setup: s.setup,
      platform: r.platform,
      detected: r.found.length,
      font_hash: createHash('sha256').update(r.found.join(',')).digest('hex').slice(0, 10),
      generics_identical: new Set(r.generic_sizes).size === 1,
      fonts: r.found,
    });
  } finally {
    await browser.close();
  }
}
console.log(JSON.stringify(results, null, 2));

What we got

Setupnavigator.platformDetected (of 46)List hashGenerics identicalFamilies found
os: windows (launch 1)Win32226564fcf93fnoArial, Arial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana, Helvetica
os: windows (launch 2)Win32226564fcf93fnoArial, Arial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana, Helvetica
os: macosMacIntel2323e513dd7anoArial, Arial Black, Comic Sans MS, Courier New, Georgia, Impact, Microsoft Sans Serif, Tahoma, Times New Roman, Trebuchet MS, Verdana, American Typewriter, Avenir, Avenir Next, Futura, Geneva, Gill Sans, Helvetica, Helvetica Neue, Lucida Grande, Menlo, Monaco, Optima
os: linuxLinux x86_6413f80842cabfnoArial, Calibri, Cambria, Courier New, Palatino Linotype, Times New Roman, Helvetica, DejaVu Sans, DejaVu Serif, Liberation Sans, Liberation Mono, Noto Sans, Ubuntu
windows + fontsWin322521080195c2noArial, Arial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana, Helvetica, Helvetica Neue, Menlo, Noto Sans
windows + fonts + custom_fonts_onlyWin3234b85191c2byesCalibri, Segoe UI, Noto Sans

From the Node.js run on 2026-10-30. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
[
  {
    "setup": "os: windows (launch 1)",
    "platform": "Win32",
    "detected": 22,
    "font_hash": "6564fcf93f",
    "generics_identical": false,
    "fonts": [
      "Arial",
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "Helvetica"
    ]
  },
  {
    "setup": "os: windows (launch 2)",
    "platform": "Win32",
    "detected": 22,
    "font_hash": "6564fcf93f",
    "generics_identical": false,
    "fonts": [
      "Arial",
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "Helvetica"
    ]
  },
  {
    "setup": "os: macos",
    "platform": "MacIntel",
    "detected": 23,
    "font_hash": "23e513dd7a",
    "generics_identical": false,
    "fonts": [
      "Arial",
      "Arial Black",
      "Comic Sans MS",
      "Courier New",
      "Georgia",
      "Impact",
      "Microsoft Sans Serif",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "American Typewriter",
      "Avenir",
      "Avenir Next",
      "Futura",
      "Geneva",
      "Gill Sans",
      "Helvetica",
      "Helvetica Neue",
      "Lucida Grande",
      "Menlo",
      "Monaco",
      "Optima"
    ]
  },
  {
    "setup": "os: linux",
    "platform": "Linux x86_64",
    "detected": 13,
    "font_hash": "f80842cabf",
    "generics_identical": false,
    "fonts": [
      "Arial",
      "Calibri",
      "Cambria",
      "Courier New",
      "Palatino Linotype",
      "Times New Roman",
      "Helvetica",
      "DejaVu Sans",
      "DejaVu Serif",
      "Liberation Sans",
      "Liberation Mono",
      "Noto Sans",
      "Ubuntu"
    ]
  },
  {
    "setup": "windows + fonts",
    "platform": "Win32",
    "detected": 25,
    "font_hash": "21080195c2",
    "generics_identical": false,
    "fonts": [
      "Arial",
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "Helvetica",
      "Helvetica Neue",
      "Menlo",
      "Noto Sans"
    ]
  },
  {
    "setup": "windows + fonts + custom_fonts_only",
    "platform": "Win32",
    "detected": 3,
    "font_hash": "4b85191c2b",
    "generics_identical": true,
    "fonts": [
      "Calibri",
      "Segoe UI",
      "Noto Sans"
    ]
  }
]

Takeaways