speechSynthesis.getVoices() exposes your installed text-to-speech voices with no permission prompt — a stable, rarely-blocked fingerprinting signal.
Every operating system ships with a set of built-in text-to-speech voices, and every browser exposes the full list to any page that asks — no click, no permission dialog, no camera-style prompt. window.speechSynthesis.getVoices() was built so screen readers and read-aloud features could pick a voice, but the list it returns is also a quietly effective fingerprinting signal: it reflects your operating system, your language packs, and your browser vendor all at once, and it survives a cookie clear without blinking.
Key Takeaways
speechSynthesis.getVoices()requires no permission and returns the full list of installed text-to-speech voices to any script that calls it.- Each voice exposes
name,lang,localService, anddefault— and the exact set, order, and count of voices varies by operating system, installed language packs, and browser vendor. - The list often loads asynchronously: the first call can return an empty array, and a script has to wait for the
voiceschangedevent to see the real inventory — a quirk that itself varies by browser. - Voice inventories are cross-checkable against your claimed OS and browser, the same way GPU strings are — a "macOS Safari" user agent with a Windows-only voice list is a mismatch worth flagging.
- The signal is stable across sessions and unaffected by clearing cookies or private browsing, since it comes from installed software rather than stored data.
What is speech synthesis fingerprinting?
Speech synthesis fingerprinting reads the list of text-to-speech voices your browser can see through the Web Speech API's synthesis interface, part of the same family of APIs that powers screen readers and read-aloud browser extensions. The call itself, SpeechSynthesis.getVoices(), is synchronous, requires no permission, and returns an array of SpeechSynthesisVoice objects describing every voice the browser can hand to speak().
Nothing is spoken and no audio is produced during collection — it is a pure inventory read, closer in spirit to font enumeration or media device enumeration than to audio or canvas fingerprinting, which measure how something renders. Here, what matters is simply which optional named resources are installed — and that list differs enough between machines to be useful on its own, and more useful stacked with other signals.
What each voice exposes
Every entry in the array is a SpeechSynthesisVoice with four readable fields:
name— a human-readable label, such as "Microsoft David - English (United States)" or "Samantha"lang— a BCP 47 language tag, such asen-USorzh-CNlocalService—trueif the voice is processed on-device,falseif it depends on a remote network service (common for some Chrome-bundled Google voices)default—truefor the voice the browser would pick if none is specified
function getVoiceFingerprint() {
const voices = speechSynthesis.getVoices();
return voices
.map(v => `${v.name}|${v.lang}|${v.localService}`)
.sort()
.join(';');
}
A script does not need to call speak() at all — reading the array and hashing the sorted name/lang/localService tuples is enough to produce a stable identifier for that machine's voice inventory.
The asynchronous loading gotcha
The one wrinkle trackers (and developers) have to work around is timing. On some browsers, calling getVoices() immediately after page load returns an empty array, because the browser hasn't finished asking the operating system for its installed voices yet. The fix, documented on MDN, is to listen for the voiceschanged event and re-read the list once it fires:
function collectVoicesReliably() {
return new Promise((resolve) => {
let voices = speechSynthesis.getVoices();
if (voices.length) return resolve(voices);
speechSynthesis.onvoiceschanged = () => {
resolve(speechSynthesis.getVoices());
};
});
}
This quirk is not just a developer annoyance — it is itself a fingerprinting signal. Whether a browser needs the voiceschanged workaround, and how long the list takes to populate, differs by browser engine and platform, adding a small extra dimension beyond the voice list itself.
How distinctive and stable is it?
Voice inventories vary along three independent axes, and each one adds separation between profiles:
| Axis | Why it varies |
|---|---|
| Operating system | Windows, macOS, and Linux each ship a different default voice set under different names |
| Installed language packs | A machine set up for multiple languages exposes more voices than a single-language default install |
| Browser vendor | Some browsers add their own bundled or network-backed voices (localService: false) on top of the OS list; others expose only what the OS provides |
Because the list reflects real installed software rather than a rendering quirk, it behaves less like a random per-device noise source and more like a structured inventory — closer to checking "which fonts are installed" than "how does this GPU round floating-point math." A default single-language install produces a short, common list shared by many similar machines; a multilingual power-user setup with several installed voice packs stands out. Combined with signals like WebGL and installed fonts, it adds another largely independent axis to the overall fingerprint, following the same combinatorial logic covered in our browser fingerprinting guide: no single signal is decisive, but independent signals multiply.
It is also a useful consistency check. A voice list built entirely from Windows SAPI-style names alongside a user agent claiming macOS is the kind of contradiction that flags a spoofed or misconfigured profile, in the same family as the GPU/OS mismatches covered in Fingerprint Hardware Mismatch — the voice inventory has to agree with everything else the browser reports about itself.
Stability-wise, the list rarely changes for a given machine. It shifts only when the operating system updates its bundled voices, the user installs or removes a language pack, or a major browser update adds new bundled voices — otherwise it is a durable, cross-session identifier that a cookie clear, private window, or cache wipe does nothing to reset.
Mitigations
- Privacy-focused browser configurations. Browsers and modes designed around uniformity — Firefox's fingerprinting-resistance settings and the Tor Browser — aim to normalize or restrict distinctive per-machine API surfaces like this one, at the cost of behaving differently from a stock browser on sites that expect the full voice list.
- Understand the limits of private browsing. As with audio and canvas fingerprinting, incognito or private mode does not change installed software, so it does not change your voice list either.
- Extensions that block or spoof the Web Speech API can flatten the signal, but check that they don't simply return an empty array in a way that is itself unusual and therefore identifying — the anti-fingerprinting paradox covered in our browser fingerprinting guide applies here too.
There is no setting that shrinks your installed voice list to "common" without either removing language packs you actually use or adopting a browser mode built around blending into a large, uniform crowd.
How to test your own signal
You can see your own voice fingerprint without writing any code:
- Run a fingerprinting test tool. BrowserInsight's fingerprint check surfaces the signals your browser exposes alongside canvas, WebGL, and audio, and the EFF's Cover Your Tracks project shows how rare your overall configuration is among recent visitors.
- Compare across browsers on one machine. Open the MDN
getVoices()demo in two different browsers on the same computer — the count of voices and the presence of network-backed (localService: false) entries typically differ, even though the OS-level voices overlap. - Check browser support before relying on it. Coverage of the Web Speech API's synthesis half is broad but not universal across older browser versions — see caniuse for current support.
Frequently Asked Questions
Does speechSynthesis.getVoices() require a permission prompt?
No. Unlike camera or microphone access, reading the installed voice list needs no user permission and triggers no dialog. Any script on any page can call it and read the result immediately (subject to the asynchronous loading quirk described above).
Why does getVoices() sometimes return an empty array?
Some browsers fetch the voice list from the operating system asynchronously after the page loads, so a call made too early returns nothing. The standard fix is to listen for the voiceschanged event and re-read the list once it fires, as shown in the code example above.
Can speech synthesis fingerprinting identify me on its own?
Rarely by itself — a common single-language OS/browser combination produces a voice list shared by many people. It becomes meaningful combined with other signals like fonts, canvas, and WebGL, and as a consistency check against your claimed operating system.
Does clearing cookies or using private mode reset my voice fingerprint?
No. The voice list reflects software installed on your device, not stored browser data, so clearing cookies, using a private window, or switching profiles leaves it unchanged.
Conclusion
Speech synthesis fingerprinting turns a screen-reader accessibility API into a quiet, permission-free inventory read of your operating system's and browser's installed voices. On its own it separates browsers into loose groups by platform and language setup; combined with fonts, canvas, and WebGL, it adds one more independent axis — and one more consistency check — to the overall fingerprint. As with most fingerprinting surfaces, there's no single switch that erases it, only browser modes and extensions that reduce how much it stands out.
Recommended Reading:


