Back to Blog
Fingerprint

Frame Rate Fingerprinting: How Display Refresh Rate Tracks You

How requestAnimationFrame timing and display refresh rates create fingerprint signals, and techniques to control frame rate at the engine level.

Introduction

The requestAnimationFrame API was designed to help developers create smooth animations by synchronizing drawing operations with the display's refresh cycle. Instead of using arbitrary timers like setTimeout, requestAnimationFrame calls a function before the next screen repaint, typically at the monitor's native refresh rate. This results in smoother animations, better battery efficiency, and a more responsive user experience.

However, the callback frequency of requestAnimationFrame directly reveals the display's refresh rate. A standard 60 Hz monitor fires callbacks approximately every 16.67 milliseconds, while a 144 Hz gaming monitor fires them every 6.94 milliseconds. A 120 Hz display, a 240 Hz display, and a variable refresh rate (VRR) display each produce measurably different callback intervals. This information is accessible to any website without permissions, making it a reliable and persistent fingerprinting signal.

Privacy Impact

Display refresh rate is a hardware characteristic that users rarely think about in the context of privacy. Unlike cookies or IP addresses, the refresh rate is tied to physical monitor hardware and does not change between browser sessions, private browsing modes, or after clearing site data.

The fingerprinting risk has increased significantly as display technology has diversified. A decade ago, nearly all consumer monitors ran at 60 Hz, providing little distinguishing information. Today, the market includes 60 Hz, 75 Hz, 90 Hz, 120 Hz, 144 Hz, 165 Hz, 240 Hz, and 360 Hz panels. Multi-monitor setups may report different rates depending on which display the browser window occupies. Laptops with dynamic refresh rate features (like Apple's ProMotion at 120 Hz) add further variation.

This diversity means that frame rate information now carries meaningful entropy. A visitor with a 165 Hz monitor is significantly rarer than one with a 60 Hz display, making the refresh rate a valuable signal for narrowing down identity. When combined with screen resolution, color depth, GPU information, and other display-related signals, the frame rate contributes to a detailed hardware profile.

The problem is compounded by the fact that multiple APIs leak this information. Besides requestAnimationFrame timing, CSS animations, the Screen API, and even the timing of setTimeout callbacks (which the browser aligns to the display cycle) can all reveal the refresh rate.

Technical Background

How requestAnimationFrame Exposes Refresh Rate

When a website calls requestAnimationFrame(callback), the browser invokes the callback function before each screen repaint. The callback receives a DOMHighResTimeStamp parameter indicating when the frame began. By measuring the interval between consecutive timestamps, a website can determine the display's refresh rate with high accuracy.

The measurement is straightforward: collect 30 to 60 frame timestamps, compute the average interval, and derive the frequency. With as few as 10 frames, the refresh rate can be determined reliably. The measurement takes less than 200 milliseconds on a 60 Hz display and is invisible to the user.

CSS Animations and Transitions

CSS animations run at the display's refresh rate by default. A @keyframes animation that takes exactly one second will produce 60 intermediate frames on a 60 Hz display and 144 frames on a 144 Hz display. While the CSS specification does not guarantee frame-for-frame accuracy, the timing of animation events (animationstart, animationend, transition timing) still leaks refresh rate information.

Variable Refresh Rate Complexity

Modern displays with variable refresh rate technology (G-Sync, FreeSync, ProMotion) can change their refresh rate dynamically based on content. This creates an additional fingerprinting dimension: not only the base refresh rate but the adaptive behavior pattern becomes a signal. A display that shifts between 48 Hz and 120 Hz based on content activity produces a characteristic timing pattern that is different from a fixed-rate display.

Multi-Monitor Configurations

When a browser window spans multiple monitors or moves between displays with different refresh rates, the requestAnimationFrame timing shifts accordingly. This transition pattern can reveal multi-monitor configurations, which are themselves a fingerprinting signal.

Common Protection Approaches and Their Limitations

VPNs and Proxy Servers

VPNs have no effect on display refresh rate. Frame rate is a local hardware property measured entirely within the browser's rendering pipeline. Network-level privacy tools cannot modify it.

Incognito and Private Browsing

Private browsing modes do not alter the display's refresh rate or the behavior of requestAnimationFrame. The frame rate fingerprint in incognito is identical to the fingerprint in a normal window.

Browser Extensions

Extensions that attempt to modify requestAnimationFrame behavior face significant challenges:

  • Wrapping the callback: An extension can intercept requestAnimationFrame and throttle callbacks to a target rate (e.g., 60 FPS). However, this creates visible stuttering in legitimate animations and is detectable by comparing requestAnimationFrame timing with CSS animation timing, which the extension may not control.
  • Modifying timestamps: Altering the DOMHighResTimeStamp passed to callbacks can simulate a different refresh rate, but inconsistencies with performance.now(), Date.now(), and CSS transition timing reveal the modification.
  • Blocking the API: Disabling requestAnimationFrame entirely breaks most modern web applications that rely on it for rendering.

The fundamental problem is that extensions operate at the JavaScript API layer and cannot control the actual rendering pipeline. The display's real refresh rate influences multiple browser subsystems simultaneously, and modifying one API without controlling all of them creates detectable inconsistencies.

Browser-Level Throttling

Some browsers offer frame rate limiting in developer settings. This approach caps the rendering rate but does not provide fine-grained control, cannot be set per-session, and often introduces visual artifacts that indicate throttling.

BotBrowser's Engine-Level Approach

BotBrowser controls frame rate at the browser engine's rendering pipeline level through the --bot-fps flag. This is not a JavaScript wrapper or API interceptor. The rendering loop itself operates at the specified rate, ensuring that all frame-rate-dependent signals are internally consistent.

Profile-Based Frame Rate

When using the profile mode, BotBrowser reads the target frame rate from the loaded fingerprint profile:

chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=profile \
       --user-data-dir="$(mktemp -d)"

The profile defines the display's expected refresh rate based on the target device. A profile representing a standard office laptop reports 60 FPS. A gaming desktop profile might report 144 FPS. The rendering pipeline runs at this rate, so requestAnimationFrame callbacks, CSS animations, and all other frame-dependent timing align with the profile's display characteristics.

Custom Frame Rate

For specific use cases, you can set an exact frame rate:

chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=60

This forces the rendering pipeline to 60 FPS regardless of the host display's actual refresh rate. All timing-dependent measurements will report approximately 16.67 ms per frame.

Real Display Rate

When you want the browser to use the actual display's refresh rate:

chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=real

Engine-Level Consistency

Because the control is applied at the rendering pipeline level, all frame-rate-dependent signals are consistent:

  • requestAnimationFrame callbacks fire at the controlled rate
  • Frame timestamps match the expected interval
  • CSS animations advance at the correct pace
  • performance.now() timing during animation frames aligns with the reported rate
  • There are no discrepancies between JavaScript-measured frame rate and actual rendering behavior

Configuration and Usage

Basic CLI Usage

# Use profile-defined frame rate
chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=profile \
       --user-data-dir="$(mktemp -d)"

# Fixed 60 FPS
chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=60

# Actual display rate
chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=real

Playwright Integration

const { chromium } = require('playwright-core');

(async () => {
  const browser = await chromium.launch({
    executablePath: '/path/to/botbrowser/chrome',
    args: [
      '--bot-profile=/path/to/profile.enc',
      '--bot-fps=60',
    ],
    headless: true,
  });

  const context = await browser.newContext({ viewport: null });
  const page = await context.newPage();
  await page.goto('https://example.com');
  // requestAnimationFrame will fire at 60 FPS
  await browser.close();
})();

Puppeteer Integration

const puppeteer = require('puppeteer-core');

(async () => {
  const browser = await puppeteer.launch({
    executablePath: '/path/to/botbrowser/chrome',
    args: [
      '--bot-profile=/path/to/profile.enc',
      '--bot-fps=profile',
    ],
    headless: true,
    defaultViewport: null,
  });

  const page = await browser.newPage();
  await page.goto('https://example.com');
  await browser.close();
})();

Combined with Timing Protection

For complete hardware signal control, combine frame rate control with timing scale:

chrome --bot-profile="/path/to/profile.enc" \
       --bot-fps=60 \
       --bot-time-scale=1.0 \
       --bot-noise-seed=42 \
       --user-data-dir="$(mktemp -d)"

Verification

After launching BotBrowser with a configured frame rate, verify the result:

const measuredFps = await page.evaluate(() => {
  return new Promise(resolve => {
    const timestamps = [];
    function frame(ts) {
      timestamps.push(ts);
      if (timestamps.length < 61) {
        requestAnimationFrame(frame);
      } else {
        const intervals = [];
        for (let i = 1; i < timestamps.length; i++) {
          intervals.push(timestamps[i] - timestamps[i - 1]);
        }
        const avgInterval = intervals.reduce((a, b) => a + b) / intervals.length;
        resolve({
          fps: Math.round(1000 / avgInterval),
          avgInterval: avgInterval.toFixed(2),
          minInterval: Math.min(...intervals).toFixed(2),
          maxInterval: Math.max(...intervals).toFixed(2),
        });
      }
    }
    requestAnimationFrame(frame);
  });
});

console.log('Measured FPS:', measuredFps.fps);
console.log('Average interval:', measuredFps.avgInterval, 'ms');

What to check:

  1. Measured FPS matches the configured value (e.g., 60 for --bot-fps=60)
  2. Frame intervals are consistent with the target rate
  3. Multiple measurement methods (requestAnimationFrame, CSS animation timing) produce the same result
  4. Fingerprint testing tools report the expected refresh rate

Best Practices

  1. Match FPS to profile hardware. A profile representing a standard office laptop should use 60 FPS. A gaming desktop profile can use 120 or 144. Mismatched values weaken fingerprint coherence.

  2. Use --bot-fps=profile for automated workflows. This automatically selects the correct frame rate from the profile, reducing configuration errors.

  3. Combine with --bot-time-scale. Frame rate and execution timing should align. A slow device profile with a 60 FPS frame rate should have timing characteristics that match entry-level hardware.

  4. Avoid arbitrary FPS values. Choose values that correspond to real display hardware: 24, 30, 48, 60, 75, 90, 120, 144, 165, 240. Values like 73 or 137 do not correspond to any real monitor and would stand out.

  5. Test in headless mode. Headless browsers have no physical display, so the frame rate is entirely determined by BotBrowser's configuration. Verify that the configured FPS is reported correctly in headless mode.

Frequently Asked Questions

Does frame rate fingerprinting work on mobile devices?

Yes. Mobile devices have varying refresh rates (60 Hz, 90 Hz, 120 Hz), and many modern phones support dynamic refresh rate switching. requestAnimationFrame timing reveals the current refresh rate on mobile browsers just as it does on desktop.

Can websites detect that FPS is being controlled?

If the control is applied at the JavaScript level (wrapping requestAnimationFrame), yes, because CSS animations and other timing sources may report a different rate. BotBrowser controls FPS at the rendering pipeline level, so all timing sources are consistent.

Does --bot-fps affect video playback?

Video playback uses a separate decoding pipeline and is not affected by the --bot-fps flag. Videos play at their encoded frame rate. Only the rendering loop and animation timing are controlled.

What happens with variable refresh rate content?

BotBrowser applies a fixed frame rate based on the configuration. This is consistent with how most real browsers behave when a fixed-rate monitor is used, which is still the majority of displays.

Can I use different FPS values for different tabs?

The --bot-fps flag applies to the entire browser instance. For different frame rates, launch separate browser instances with different configurations.

How does frame rate interact with the visibility API?

Browsers typically throttle requestAnimationFrame for background tabs. BotBrowser respects this behavior unless --bot-always-active is used, which keeps all tabs running at the configured frame rate regardless of visibility state.

Does the Screen.refreshRate property match?

On browsers that expose screen.refreshRate or similar properties, BotBrowser ensures these values are consistent with the frame rate configured by --bot-fps and the loaded profile.

Summary

Display refresh rate, measurable through requestAnimationFrame timing, CSS animations, and related APIs, is a persistent hardware fingerprinting signal that standard privacy tools cannot address. BotBrowser controls frame rate at the browser engine's rendering pipeline level through the --bot-fps flag, ensuring all frame-dependent signals are internally consistent. Combined with performance timing protection, Canvas control, and comprehensive profile management, BotBrowser provides complete hardware signal protection for privacy-focused workflows.

#fps#frame-rate#requestAnimationFrame#fingerprinting#privacy#display