What SharedArrayBuffer Timing Attack Fingerprinting Actually Measures
A sharedarraybuffer timing attack fingerprinting check does not read a value you left behind — it times work your machine performs. How fast a Worker increments a counter, how long a GPU draw takes next to a software render, how much an SSD stalls under load. Cross-origin isolation is what unlocks the finest clock, so the isolation state itself becomes part of the signal. Here is the mechanism, the headers, the real limits, and what to fix.
📌 TL;DR Executive Summary
- Core Takeaway: SharedArrayBuffer plus Atomics polling builds a clock finer than the clamped
performance.now()can give, but only inside a cross-origin isolated document. - Key Risk/Challenge: Isolation needs COOP: same-origin and COEP: require-corp together. It blocks assets, kills OAuth popup handshakes silently, and report-only staging leaves your fallback code paths unvalidated.
- Recommended Solution: Feature-detect
self.crossOriginIsolated, stage with report-only headers, keep hardware claims consistent with measured timing, and give each identity its own isolated browser profile.
SharedArrayBuffer as a High-Resolution Clock
SharedArrayBuffer gives two JavaScript agents — normally the page and a Worker — access to the same block of memory. You allocate a few bytes, hand the buffer to the Worker through postMessage, and both sides read and write the same addresses without copying. That is the whole feature, Baseline widely available since December 2021, with one condition attached.
The fingerprinting value is in the polling, not the memory. A Worker can increment a counter in a tight loop while the page reads that counter thousands of times per millisecond, and Atomics.wait() parks a thread until a value changes, which turns the buffer into a stopwatch. The resolution you get depends on how fast the engine schedules threads, not on the clamp the specification imposes on performance.now().
That is why the feature was pulled back after Spectre and returned only behind cross-origin isolation. The buffer is not the danger; the improvised clock is. So the honest definition of a sharedarraybuffer timing attack fingerprinting probe is this: it times operations on your machine with a clock that exists only inside an isolated document, then compares that timing shape against known devices, rendering modes and hardware profiles.
Timing is rarely the only signal in play. It corroborates the surface identifiers you already know about, which is why it helps to understand what a browser fingerprint is assembled from before treating timing as a separate problem.
How the Technique Works Under the Hood
COOP and COEP are a logical AND
Isolation never turns on because you set one header. A document must serve both Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on the same response. With only one of them, self.crossOriginIsolated stays false and typeof SharedArrayBuffer returns "undefined" — while COEP alone still blocks cross-origin subresources that lack Cross-Origin-Resource-Policy: cross-origin or a valid CORS pass. You get the breakage without the features.
| Headers served | crossOriginIsolated | SharedArrayBuffer | Cross-origin assets |
|---|---|---|---|
| COOP only | false | undefined | Load normally |
| COEP only | false | undefined | Blocked without CORP or CORS |
| COOP + COEP: require-corp | true | Available | Blocked without CORP or CORS |
| COOP + COEP: credentialless | true | Available | Sent no-CORS, without cookies |
Because the gate is a runtime flag, it also doubles as a probe. Any page can read crossOriginIsolated and learn something about the embedding context, the response headers and the engine in front of it.
The two-tier clamp on performance.now()
performance.now() returns a monotonic timestamp in milliseconds as a float, relative to Performance.timeOrigin. The High Resolution Time spec defines two tiers: roughly 100 microseconds in an ordinary page and roughly 5 microseconds once the document is cross-origin isolated. Both are written as a minimum resolution “or higher”, so an engine is free to clamp harder than the floor.
The spec names cache attacks, statistical fingerprinting and micro-architectural attacks as the motivation, and defines a coarsen time algorithm every implementation must apply, with optional jitter and throttling of repeated calls on top. Historical engine behaviour diverged sharply — Chrome settled near 5 microseconds with Site Isolation, Firefox near 1 millisecond, Safari around 100 microseconds — so the tier you land in is partly a browser-version signal. You can read the current definition in MDN’s performance.now() reference.
Date.now() sits outside that system entirely. It tracks wall-clock time, was capped at one millisecond before Spectre, and gains no precision from isolation. When a script needs finer timing than performance.now() offers, shared memory is the usual fallback.
When the local clock freezes, borrow a remote one
In August 2026 Cloudflare reported that under CPU-only execution inside Workers, both Date.now() and performance.now() were frozen, yet a WebSocket to an external timestamp server still recovered sub-millisecond timing. That result describes server-side isolates, not browsers, so borrow the lesson rather than the numbers: clamping a local clock removes one source of timing signal, not the idea of timing. If a page can reach any remote clock, it has a coarse one available.
performance.now() in a tight loop, measures its own jitter and reports the smallest non-zero tick is describing which tier it landed in. A resolution that looks self-consistent but unusually narrow tells a checker more than any individual duration you measure.
What timing reveals about hardware and storage
Render timing separates hardware from software rendering. A draw that takes roughly 2 ms on a real GPU can take 50–200 ms under Chrome’s SwiftShader software renderer, so a page timing a canvas draw can distinguish a physical GPU from a VM or a headless container. Those figures come from a vendor question-and-answer page rather than a published benchmark — treat them as directional, but note the shape of the signal: the gap is wide enough that no statistical subtlety is required.
Storage timing is the newer frontier. An attack family called FROST abuses the Origin Private File System API together with high-resolution timers to force real disk I/O and read SSD contention from JavaScript, with no extra permissions. Its authors reported macro-averaged F1 of 88.95 in a closed-world test and 86.95 in an open-world test across the top 50 Alexa sites, F1 95.83 for distinguishing ten pre-installed macOS applications at startup, and a covert channel decoding up to tens of kilobits per second on Linux and macOS. Those figures come from one research group and may not generalise to production sites, and contention is noisy on multi-disk workstations or RAM-backed volumes. The mechanism itself is real.
Hardware claims follow the same logic. navigator.hardwareConcurrency reports logical core count and navigator.deviceMemory reports RAM rounded to the nearest power of two. Neither value changes under a proxy, and both can be contradicted by measurement: a Worker loop that crawls looks like a one-core machine even when the surface claims sixteen.
For a deeper look at the header mechanics, migration staging and the silent handshake failures described here, the COOP/COEP migration walkthrough is worth reading end to end before you touch a production response.
Why It Matters for Multi-Account Operations
If you run many accounts for e-commerce, ads or social media, timing corroborates rather than replaces the obvious signals. Anti-bot systems already read canvas, WebGL, fonts and audio; timing confirms or denies the story those signals tell. A profile claiming workstation hardware that renders at software speeds, or claiming a modern laptop while Worker throughput says single core, is internally inconsistent — and inconsistency is what gets challenged.
A proxy does not help here. Residential and mobile proxies change the network path and the exit IP, which is essential for geolocation, timezone and WebRTC coherence, but they change nothing about clock resolution, core count or GPU behaviour. People buy good proxies, then keep running every identity in the same browser on the same laptop, and wonder why timing-based checks still group the accounts. The relative weight of each signal is covered in this breakdown of browser fingerprint entropy.
There is a second audience: teams that have to switch isolation on. COOP: same-origin puts the document in its own browsing context group, so window.opener reads null for cross-origin windows — OAuth popups, payment sheets and postMessage handshakes do not throw, they fail silently. COEP: require-corp then blocks any cross-origin subresource that does not send Cross-Origin-Resource-Policy: cross-origin or arrive in CORS mode with a valid Access-Control-Allow-Origin. Assets that loaded fine yesterday stop loading.
COEP: credentialless isolates the document but sends no-CORS cross-origin requests stripped of cookies and client certificates. Public CDN assets load; anything credentialed returns an empty response. It is also not supported in Safari, so never ship it without measuring the behavioural difference per engine.
A Practical Checklist
- Feature-detect before assuming. Check
self.crossOriginIsolatedandtypeof SharedArrayBufferbefore any code path relies on fine timers, and decide explicitly what the fallback does. - Stage with report-only headers.
COOP-Report-OnlyandCOEP-Report-OnlywithReporting-Endpointsevaluate the policy and report every resource that would be blocked without blocking anything. Delivery is Chromium-only, so coverage is partial. - Plan for third-party assets you cannot header. Proxy them through your own origin, drop them from isolated documents, or fall back to
COEP: credentialless. - Relocate cross-origin window handshakes. Move OAuth and payment flows to same-origin pages or server-side redirects, because
window.openerwill benull. - Audit your own timer usage. Logic that quietly depends on 5-microsecond precision behaves differently the day isolation lands.
- Compare secrets in constant time. An early-exit
==leaks how many leading bytes matched, which is the classic timing side channel. Use library primitives instead of hand-rolled loops. - Test each profile with a timing harness, not just a canvas test. Hold
hardwareConcurrencyagainst measured Worker throughput and confirm render timing matches the GPU you claim. Most browser fingerprint test tools do not report timing distributions, so run your own loop inside each profile.
Feature detection itself is short. Run this anywhere you are auditing, including a profile you are about to trust:
// Which timer tier does this document actually get?
console.log("isolated:", self.crossOriginIsolated);
console.log("SharedArrayBuffer:", typeof SharedArrayBuffer);
let smallest = Infinity;
let prev = performance.now();
for (let i = 0; i < 100000; i++) {
const t = performance.now();
const delta = t - prev;
if (delta > 0 && delta < smallest) smallest = delta;
prev = t;
}
console.log("smallest non-zero tick (ms):", smallest);
Constant-time comparison, for the server side of the same problem:
import hmac
def verify(token: str, expected: str) -> bool:
# compare_digest takes the same time regardless of where the first
# mismatching byte sits; == returns early and leaks that position.
return hmac.compare_digest(token.encode(), expected.encode())
Common Mistakes
- Setting one isolation header and expecting SharedArrayBuffer. The condition is an AND. With COOP alone or COEP alone,
crossOriginIsolatedstays false and the constructor stays undefined. - Trusting a report-only phase to validate fallbacks.
crossOriginIsolatedstays false for the entire report-only period, so every feature-detected branch keeps taking its fallback path. You only exercise the bright path when you enforce, and that is when production finds out. - Assuming a VPN or proxy hides timing. It changes the network path, not the clock. Core count, GPU draw time and disk contention are measured locally.
- Randomising timer readings per call. Random timestamps are incoherent, and incoherence across calls is itself a pattern. Consistency within a profile is the goal.
- Treating timing as a substitute for canvas, WebGL and font spoofing. It does not replace them, it corroborates them. Break the coherence and the check gets easier, not harder.
- Ignoring platform differences. The spec requires the clock to keep ticking while the OS sleeps, but only Windows browsers currently do, which is a platform-visible inconsistency. Safari does not support credentialless COEP at all.
If you are still narrowing down which JavaScript-level signals matter most for your setup, this walkthrough of JavaScript engine fingerprinting covers the surface that timing sits beside.
Keeping Timing Signals Coherent Across Profiles
No browser deletes timing signal, and no tool can honestly claim to. What you can control is coherence and separation: one identity per profile, with hardware, canvas, WebGL, audio and fonts spoofed at the engine level so a timing measurement never contradicts the surface it belongs to. Sendwin Browser runs a patched-Chromium engine with the Sendwin Stealth engine built in, spoofing those layers in the engine rather than through brittle script injection and keeping them coherent, so each profile reads like a separate real machine and no two profiles share a fingerprint.
You can run profiles two ways. The desktop app installs locally on Windows 10/11, macOS 12+ and Linux. The cloud browser runs profiles on EU and US cloud nodes from any device with nothing to install, with a free 10-minute-per-day preview and unlimited cloud browsing time on Pro and Team. Every plan includes built-in residential proxies plus bring-your-own HTTP/SOCKS5, and timezone, locale, WebRTC and geolocation follow the proxy’s exit IP automatically — so the network story agrees with the profile instead of arguing with it. If you audit from a second machine, cloud sync keeps logins consistent, and sharing a profile with a paid teammate opens it already signed in.
For teams scripting the audit, the local Automation API on the Team plan drives Selenium, Puppeteer and Playwright against a running profile:
from playwright.sync_api import sync_playwright
CDP_URL = "http://127.0.0.1:PORT" # copy it from the profile's automation settings
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(CDP_URL)
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("about:blank")
print(page.evaluate("() => self.crossOriginIsolated"))
print(page.evaluate("() => typeof SharedArrayBuffer"))
browser.close()
Run the tick loop from the previous section through each profile and you get a timing baseline per identity instead of one average for the whole machine.
🏆 Send.win Verdict
Timing checks are hard to beat because they measure physics, not strings. What an anti-detect browser can honestly fix is the mismatch: a profile whose claimed hardware, GPU and locale disagree with measured throughput is the profile that gets flagged. Send.win keeps those layers coherent at the engine level, isolates each profile so one identity’s timing noise cannot bleed into another, and aligns timezone and geolocation with the proxy exit IP — removing the easy contradictions without pretending to delete the clock.
Try Send.win free today — run your own timing loop inside a fresh profile and compare it with the machine you are using now.
Frequently Asked Questions
Why was SharedArrayBuffer disabled after Spectre?
Shared memory let a page build a timer finer than any timer API the browser was willing to expose, which turned speculative-execution side channels into a practical read primitive. Instead of removing the microarchitectural leaks, engines gated the feature behind cross-origin isolation, so only documents that had already sealed off cross-origin embedding could use it.
What headers are needed to make SharedArrayBuffer work again?
Both Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp must be present on the same response. Set only one and nothing changes: crossOriginIsolated stays false and typeof SharedArrayBuffer returns "undefined". Check the flag at runtime instead of assuming from configuration.
How much precision does performance.now() give now?
The spec defines roughly 100-microsecond resolution in an ordinary page and roughly 5 microseconds once cross-origin isolated, both phrased as a minimum “or higher”. Real engines vary by version and configuration, and implementations may add jitter or throttle repeated calls. Measure the tier you are getting rather than quoting the spec figure.
Does a VPN hide timing fingerprints?
No. A VPN or residential proxy changes your IP and network path, which matters for geolocation and timezone coherence, but it does not change timer resolution, logical core count, GPU draw time or disk contention. Those are local measurements, so they stay with the machine wherever the traffic exits.
Why do OAuth popups break after enabling COOP?
COOP: same-origin places the document in its own browsing context group, so window.opener reads null for cross-origin windows. The popup still opens; the handshake back to the opener never arrives, and it fails silently rather than throwing. Move those flows to same-origin pages or server-side redirects.
How do you test COEP without breaking the page?
Serve COOP-Report-Only and COEP-Report-Only with a Reporting-Endpoints header and collect the reports: you get a list of every resource that would be blocked and nothing breaks. Remember that delivery is Chromium-only, and that crossOriginIsolated stays false throughout the report-only phase, so SharedArrayBuffer paths cannot be validated until you enforce.
How do anti-bot systems detect headless browsers through render timing?
They time a GPU-bound draw. A real GPU finishes in roughly 2 milliseconds, while software rendering under Chrome’s SwiftShader can take 50–200 milliseconds — a gap large enough that no statistical subtlety is needed. The same idea extends to Worker throughput, which exposes machines whose advertised core count does not match measured performance.
Can timing fingerprinting work across different browsers and tabs?
Timing measured in one profile says little about another, which is exactly why profile isolation matters. On a single machine, separate profiles with inconsistent hardware claims can still be grouped when their timing behaviour contradicts their surfaces. Keeping each profile’s clock resolution, core count and GPU story self-consistent is the practical defence.
How Send.win Helps With Sharedarraybuffer Timing Attack Fingerprinting
Send.win is an antidetect browser built for exactly this kind of work — every profile is a clean, isolated identity:
- Isolated profiles – unique fingerprint, separate cookies and storage per profile
- Stealth engine – canvas, WebGL, fonts, and audio spoofed at the engine level
- Desktop app + cloud sessions – native app for Windows, macOS, and Linux, or run profiles in the cloud with no install
- Built-in residential proxies – with automatic timezone, locale, and WebRTC matching
- Team features – share logged-in profiles with teammates without sharing passwords
Try the instant cloud browser demo — no install, no signup — or download the desktop app. The 30-day free trial needs no credit card, and paid plans start at $6.99/month billed annually (see pricing).