Skip to content
On this page

Speech synthesis fingerprinting reads the list of text-to-speech voices exposed by window.speechSynthesis.getVoices(). "Text-to-speech" means the built-in feature that reads text aloud, and the voices it offers are installed by your operating system, not your browser. That makes the list a strong clue about which OS and version you are running: Windows ships specific Microsoft voices, macOS/iOS ship Apple voices, and Android ships Google voices, each with characteristic names, languages, and voiceURI values (a unique ID string per voice). A headless Linux server - a browser running on a server with no screen or speech engine - typically returns an empty voice list, which both flags automation and contradicts any Windows or macOS User-Agent (the text a browser uses to say what it is).

Quick facts

APIwindow.speechSynthesis.getVoices()
RevealsOS + version via bundled TTS voice names, langs, voiceURIs, localService flag
Headless tellEmpty array on a typical headless Linux server
Coherence trapVoice set must match the claimed OS (Windows voices on a Windows UA)
QuirkOften returns [] on first call until the voiceschanged event fires

Why the voice list is an OS fingerprint

Text-to-speech voices come from the platform, not the browser, so the set is tightly tied to the operating system. Windows 11 exposes voices like Microsoft David, Microsoft Zira, and language-specific additions; macOS exposes Apple voices such as Samantha and Daniel with com.apple. voiceURIs; Android exposes Google voices. Each voice carries a name, a lang (its language), a voiceURI (its ID), a localService boolean (true if the voice runs on the device rather than in the cloud), and a default flag. The full list - which voices, in what order, with which languages - forms a recognisable per-OS, often per-version signature.

That makes it a useful way to double-check what platform a browser claims to be, and a relatively high-entropy signal - meaning it narrows down who you are quite a lot - because the exact voice set changes with OS version and installed language packs.

The empty list and the coherence trap

Headless browsers on Linux servers usually have no speech engine installed, so getVoices() returns [] (an empty list). An empty voice list is unusual for a real desktop user and, worse, it contradicts a Windows or macOS User-Agent, which should come with a known set of voices. So this one check does two jobs: an empty list hints at a headless bot, and a mismatched list (macOS voices under a Windows User-Agent, or the reverse) hints at platform spoofing - lying about which OS you run.

There is a well-known timing quirk: in Chrome the first call to getVoices() often returns [] because the engine loads the voices in the background and only signals it is ready by firing a voiceschanged event. Detection scripts know this and wait for that event, so a spoofing tool that just hands back a fixed list - without the realistic background loading - can itself look suspicious. The reliable fix, as with fonts and audio fingerprinting, is to expose a coherent, real voice list for the OS you are claiming, rather than a generic or empty one.

Code example

javascript
// Voice-list fingerprint (handle the async population quirk)
function getVoiceFingerprint() {
  return new Promise(resolve => {
    let v = speechSynthesis.getVoices();
    if (v.length) return resolve(summ(v));
    speechSynthesis.onvoiceschanged = () => resolve(summ(speechSynthesis.getVoices()));
  });
  function summ(voices) {
    return voices.map(x => x.name + '|' + x.lang + '|' + (x.localService ? 'L' : 'R'))
                 .sort().join(',');
  }
}
// [] on headless Linux. Windows UA with macOS 'com.apple.*' voices = spoof tell.

End of path

That is all 8 entries in Rendering and hardware surfaces.

The fingerprint signals produced by the GPU, the audio stack, and the font rasteriser.

Browse all topics

Related terms

Concept map

Concept map

How Speech Synthesis Fingerprinting connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

Why does speechSynthesis.getVoices() reveal my operating system?

Because the voices are installed by the OS, not the browser. Windows, macOS, Android, and Linux each ship a characteristic set of voice names, languages, and voiceURIs (the per-voice ID strings). The list is therefore a strong clue about your OS and version, and it must agree with your User-Agent - voices that do not match the claimed OS point to platform spoofing.

Why did getVoices() return an empty array even on a real browser?

Chrome loads the voice list in the background, so the very first call often returns [] until the voiceschanged event fires to say the list is ready. Real detection scripts wait for that event. The actual bot giveaway is an empty result that never fills in - common on headless Linux with no speech engine installed.

Is speech synthesis a major detection vector?

It is a secondary, supporting check rather than a primary blocker, but it carries a lot of signal: empty lists catch headless servers, and mismatched lists catch OS spoofing. It is most useful as one part of a broader consistency check across fonts, voices, and the platform a browser claims to be.

Last updated: 2026-05-31