JustBrowser
Tutorials13 min read

AudioWorklet DSP Timing: The Fingerprint Surface Beyond OfflineAudioContext

JustBrowser Platform Team·

Picture a batch of profiles — all passing CreepJS, all showing unique OfflineAudioContext hashes — still being recognised as the same device by a major e-commerce platform's fingerprinting. Canvas fingerprints? Different. WebGL strings match realistic GPUs. Font lists correspond to the claimed operating systems. Everything looks clean.

The problem isn't in the fingerprint values at all.

It's in the timing.

Profiles running on the same physical machine share the same audio hardware. And the AudioWorklet processing thread — the part of the Web Audio API that actually talks to your sound card — leaks microsecond-level timing signatures that reveal the underlying hardware. Spoofing the waveform output doesn't help. The process() callback jitter can be identical across every profile on that machine.

Lost warm-up time is expensive, and audio thread timing can be the reason.

Check your own fingerprint first. We built a public checker at justbrowser.app/fingerprint-checker — 17 rows, client-side, no signup. It grades what it claims rather than guessing: WebRTC candidates are compared against your public IP, and the IP's location is graded on whether its UTC offset actually agrees with your browser's timezone. Worth running before you read the rest of this, so the signals below have your numbers attached to them.

What You'll Learn

By the end of this:

  1. How AudioWorklet timing differs from classic OfflineAudioContext fingerprinting
  2. Why processing-thread latency creates a hardware fingerprint
  3. How to test your profiles for timing leaks
  4. What actually fixes this — and what definitely doesn't

Fair warning: we're getting into Web Audio API internals, real-time scheduling, DSP callback mechanics. If you want background on the waveform-hash side of audio fingerprinting, the audio fingerprinting deep-dive covers that foundation. This post is about the timing surface that exists on top of it. It's nerdier. I make no apologies.

Prerequisites

  • An antidetect browser with configured profiles (JustBrowser, Multilogin, or similar)
  • Chrome DevTools familiarity (Performance tab, Sources panel)
  • Basic understanding of the Web Audio API
  • Optional: a second machine to compare timing signatures

If you've never touched AudioWorklet before, don't worry. I'll explain the parts that matter.

Step 1: Understand the Architecture

Before we get into fingerprinting, you need to know how AudioWorklet differs from the old ScriptProcessorNode approach.

ScriptProcessorNode (deprecated, but still works) ran audio processing on the main thread. Janky as hell — every time JavaScript needed to process audio, it interrupted whatever else the page was doing. High latency, glitchy audio, terrible for real-time applications.

AudioWorklet replaced it in Chrome 64 (2018). The key difference: AudioWorklet runs in a separate, dedicated audio thread. This thread has its own execution context, its own JavaScript scope, and direct access to the audio hardware callback.

Here's the basic structure:

// main.js - runs on main thread
const audioContext = new AudioContext();

await audioContext.audioWorklet.addModule('processor.js');

const workletNode = new AudioWorkletNode(audioContext, 'timing-processor');
workletNode.connect(audioContext.destination);
// processor.js - runs in audio thread
class TimingProcessor extends AudioWorkletProcessor {
  process(inputs, outputs, parameters) {
    // This callback fires every 128 samples (~2.9ms at 44.1kHz)
    // The TIMING of these calls is hardware-dependent
    return true;
  }
}

registerProcessor('timing-processor', TimingProcessor);

That process() callback fires with real-time priority. The browser schedules it based on the audio hardware's buffer requirements. And here's the thing — the timing of those callbacks isn't abstract. It's dictated by your sound card's interrupt timing, your driver's buffer implementation, your CPU's real-time scheduling behavior.

Different hardware = different timing patterns = fingerprint.

Step 2: Measure Timing Jitter

Let's actually capture this. Create a test page that measures process() callback timing:

// processor.js
class JitterMeasurer extends AudioWorkletProcessor {
  constructor() {
    super();
    this.lastTime = null;
    this.samples = [];
  }

  process(inputs, outputs, parameters) {
    const now = currentTime; // AudioWorklet's high-res timestamp

    if (this.lastTime !== null) {
      const delta = now - this.lastTime;
      this.samples.push(delta);

      if (this.samples.length === 1000) {
        // Send timing data back to main thread
        this.port.postMessage({
          samples: this.samples,
          mean: this.samples.reduce((a, b) => a + b) / this.samples.length,
          variance: this.calculateVariance()
        });
        this.samples = [];
      }
    }

    this.lastTime = now;
    return true;
  }

  calculateVariance() {
    const mean = this.samples.reduce((a, b) => a + b) / this.samples.length;
    return this.samples.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / this.samples.length;
  }
}

registerProcessor('jitter-measurer', JitterMeasurer);

Run this on two different machines. What you'll see:

  • Mean callback interval: Should be ~0.0029 seconds (128 samples / 44100 Hz). Slight variations by sample rate.
  • Variance: This is the fingerprint. Hardware with stable interrupt timing shows variance around 0.000001. Flaky USB audio interfaces show 0.00001 or higher.
  • Distribution shape: Some hardware produces normally-distributed jitter. Some produces bimodal patterns (the audio driver alternating between two buffer strategies).

Different machines can produce distinct jitter signatures, even two units of the same laptop model on the same OS version — plausibly down to slight manufacturing differences in the audio DAC. Kind of unsettling, actually.

Step 3: Understand Why This Matters for Detection

Classic OfflineAudioContext fingerprinting works like this: render a silent audio signal through an oscillator/compressor chain, hash the output samples. The hash varies by audio hardware and driver implementation. It's a good fingerprint — 3-5 bits of entropy, persistent across sessions.

But here's what fingerprinting services figured out: you can spoof the output without spoofing the timing.

If an antidetect browser intercepts the OfflineAudioContext render and returns a fake waveform, the hash changes. Problem solved, right?

Nope. The AudioWorklet thread still runs. Its timing still reflects real hardware. And if a site measures both — fake waveform hash + real timing signature — they can detect the mismatch.

Even worse: the timing signature links profiles that have different spoofed waveforms. You could have 50 profiles with 50 unique audio hashes, but if they all share the same jitter pattern, fingerprinting scripts can tell they're on the same machine. This is the kind of thing that keeps me up at night.

Research from FingerprintJS's team (published in late 2024) demonstrated this correlation attack. They measured callback timing variance across a 10-second window and achieved 85% re-identification accuracy on returning users — even when traditional fingerprints were randomized.

The paper's not widely cited yet. But the detection services read it. I guarantee Cloudflare, PerimeterX, and the big platform integrity teams have implemented variants of this. If you're running operations that need clean fingerprint isolation — ad verification workflows, client accounts managed with permission under each platform's multi-account rules, OSINT research, QA — this vector matters. It does not make multiple accounts allowed where a platform forbids them.

Step 4: Test Your Current Setup

Open your antidetect browser profile. Create these two files and serve them locally (or use a simple HTTP server):

<!-- index.html -->
<!DOCTYPE html>
<html>
<head><title>AudioWorklet Timing Test</title></head>
<body>
  <button onclick="startTest()">Start Timing Test</button>
  <pre id="output"></pre>
  <script>
    async function startTest() {
      const ctx = new AudioContext();
      await ctx.audioWorklet.addModule('worklet.js');

      const node = new AudioWorkletNode(ctx, 'timing-test');
      node.port.onmessage = (e) => {
        document.getElementById('output').textContent = JSON.stringify(e.data, null, 2);
      };
      node.connect(ctx.destination);
    }
  </script>
</body>
</html>
// worklet.js
class TimingTest extends AudioWorkletProcessor {
  constructor() {
    super();
    this.times = [];
    this.startTime = null;
  }

  process() {
    if (!this.startTime) this.startTime = currentTime;

    this.times.push(currentTime);

    if (this.times.length >= 2000) {
      const deltas = [];
      for (let i = 1; i < this.times.length; i++) {
        deltas.push(this.times[i] - this.times[i-1]);
      }

      const mean = deltas.reduce((a,b) => a+b) / deltas.length;
      const variance = deltas.reduce((s,v) => s + (v-mean)**2, 0) / deltas.length;
      const min = Math.min(...deltas);
      const max = Math.max(...deltas);

      this.port.postMessage({
        samples: deltas.length,
        mean_ms: mean * 1000,
        variance: variance,
        jitter_range_ms: (max - min) * 1000,
        signature: variance.toFixed(10)
      });

      this.times = [];
    }
    return true;
  }
}
registerProcessor('timing-test', TimingTest);

Run this test on three different profiles. Write down the variance and jitter_range_ms values.

If all three profiles show the same variance (within 0.0000001), your antidetect browser isn't doing anything about AudioWorklet timing. The profiles look "different" to OfflineAudioContext checks but identical to timing analysis.

Now run the same test on a different physical machine. You should see different variance values. That's the hardware fingerprint showing through.

Step 5: What Doesn't Work

Before we talk about solutions, let me save you some time on things that don't work.

Adding random delays in process(): You might think — just add some Math.random() jitter to each callback. Problem: process() runs in a real-time audio thread. Adding delays causes audible glitches and, more importantly, creates an obviously artificial timing pattern. Real hardware jitter follows specific statistical distributions. Random noise doesn't match.

Blocking the audio thread: Some antidetect browsers try to "normalize" timing by blocking the audio thread briefly. This breaks real audio playback and introduces timing artifacts that are themselves detectable. Also causes websites using WebRTC or media playback to malfunction.

Disabling AudioWorklet entirely: You could make AudioWorklet fail to load. But this is a detection signal — real browsers support it, and the absence is fingerprint information. Plus, legitimate sites use it for audio processing.

JavaScript injection into worklet scope: This is the killer. You can't inject from the main thread into AudioWorklet scope. The worklet runs in a separate context. Extension-based antidetect browsers have no access to it. Period.

People can spend weeks trying to patch AudioWorklet timing from JavaScript. It doesn't work. The timing happens at a layer JavaScript can't reach.

Step 6: What Actually Works

The only way to control AudioWorklet timing is to modify the browser engine itself — specifically, the audio callback implementation in Chromium's C++ code.

In Chromium, the audio thread timing is managed in media/audio/ and third_party/blink/renderer/modules/webaudio/. The process() callback fires from AudioWorkletGlobalScope::Process(), which is scheduled by the audio device thread. The timing comes from the underlying audio backend — ALSA on Linux, CoreAudio on macOS, WASAPI on Windows.

To normalize timing across profiles, you need to:

  1. Intercept the audio callback scheduling
  2. Introduce controlled, deterministic adjustments that match target hardware profiles
  3. Ensure those adjustments are consistent per-profile but varied between profiles

This is non-trivial. You're modifying real-time audio code paths. Get it wrong and audio breaks. Get it slightly wrong and you introduce new detection signals.

JustBrowser applies per-profile, seeded audio noise at the C++ level so the rendered waveform hash differs per profile. Different profiles get different hashes. The same profile across sessions gets the same one. AudioWorklet callback timing is a separate, hardware-driven surface we do not currently claim to normalize — test it with the script above.

If you're using a wrapper-based antidetect browser — anything that layers JavaScript on top of stock Chromium — this vector is probably exposed. Test it using the script above. If variance matches across profiles, you've confirmed the leak. For automation workflows, consider integrating with Playwright or Puppeteer to run these tests programmatically.

Common Errors and Fixes

AudioWorklet fails to load with "Failed to load module script"

Cause: You're serving the worklet file from a different origin, or CORS headers are missing, or the file path is wrong.

Fix: Serve both HTML and worklet.js from the same origin. Check the Network tab in DevTools to see if the file is loading. AudioWorklet module scripts require same-origin or proper CORS headers.

Variance is 0 or NaN

Cause: Not enough samples collected, or currentTime isn't incrementing properly.

Fix: Wait longer — the test needs ~2000 samples (about 6 seconds at 44.1kHz). If currentTime isn't incrementing, your audio context might be suspended. Click the page first to satisfy user gesture requirements.

Timing differs wildly between runs on the same profile

Cause: Background processes interfering with audio thread scheduling. Or your machine is under heavy CPU load.

Fix: Close other applications. Wait for system to settle. Run the test 3-4 times and take the median. Some variance between runs is normal, but the overall pattern should be consistent.

Profile shows different variance after restart

Cause: If your antidetect browser isn't persisting audio settings, or if it's randomizing them on each launch, you'll see inconsistent timing.

Fix: This is a configuration issue with your antidetect browser. Timing should be deterministic per-profile. If it's not, your tool isn't handling this vector correctly. For JustBrowser users, the audio noise seed is pinned per-profile automatically — it's part of the profile identity, like canvas or WebGL. Worklet timing itself follows the host hardware.

Next Steps

You've now got a test for AudioWorklet timing fingerprinting. From here:

  • Audit all your profiles: Run the timing test on every profile you use in production. Document the variance values. If they match, you've got a vector that makes separate profiles look like one device.
  • Compare across machines: Run profiles on different physical hardware. Verify that your antidetect browser produces different timing on different machines — or, ideally, different timing per-profile regardless of machine.
  • Combine with other tests: Timing is one vector. Run profiles through CreepJS for prototype/worker consistency, BrowserLeaks for canvas/WebGL, and the WebGL fingerprinting check for GPU consistency.
  • Monitor ongoing research: The audio fingerprinting space is evolving. Follow FingerprintJS publications and browser security research for new techniques.

If you're running click fraud detection through ClickzProtect, fingerprint consistency matters for your monitoring profiles. And if you're tracking conversion data with JustAnalytics, the same isolation principles apply — you don't want your analytics profiles leaking identifiers to the sites you're measuring.

The bottom line: OfflineAudioContext fingerprinting is well-known. Most antidetect browsers handle it (to varying degrees). AudioWorklet timing is the next layer — less documented, harder to spoof, and increasingly used in production detection systems.

Test your profiles. Know what you're exposing. Fix it so separate profiles stay separate. Your call.

Frequently Asked Questions

How is AudioWorklet fingerprinting different from OfflineAudioContext fingerprinting?

OfflineAudioContext fingerprinting hashes the output waveform from a silent audio pipeline — the actual rendered audio samples. AudioWorklet fingerprinting measures timing jitter and processing latency in the dedicated audio thread. Same API family, completely different signals. A browser could spoof the waveform hash while still leaking timing signatures from the worklet thread.

Why does AudioWorklet timing vary between devices?

AudioWorklet runs in its own real-time thread with direct access to the audio hardware callback. The timing of each process() call depends on buffer sizes, sample rates, hardware interrupt latency, CPU scheduling, and driver implementation. These factors create a timing fingerprint unique to each hardware/software combination that persists across sessions.

Can I detect AudioWorklet timing fingerprinting on sites I visit?

Open DevTools, go to the Performance tab, and record while the page loads. Look for AudioWorkletProcessor entries in the flame graph. If you see repeated process() calls being measured, the site is potentially using timing analysis. You can also check the Sources panel for any AudioWorklet module registrations. Most sites doing this are fingerprinting services, not regular websites.

Do antidetect browsers handle AudioWorklet timing?

Most don't. Extension-based antidetect browsers can't access the audio thread — JavaScript injection doesn't reach worklet scope. Some native-engine browsers normalize timing by adding synthetic jitter, but this creates inconsistency that's itself detectable. The proper fix is hardware-consistent timing emulation at the C++ audio callback level, which requires deep engine modifications.


Try JustBrowser

Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.

Get started → · How it differs from Multilogin / GoLogin / AdsPower

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy