Privacy Tools Guide

HTTP/2 connection pooling creates a fingerprinting vector that websites exploit to track users across the web by analyzing connection patterns and reuse behaviors. The browser’s connection pool—a cache of persistent connections reused across multiple requests—exposes patterns unique enough to identify individual users. Developers must understand this technique to defend against it and build privacy-conscious applications.

How Browser Connection Pooling Works

When your browser visits a website, it doesn’t establish a fresh TCP connection for every request. Instead, it maintains a connection pool—a small cache of persistent connections reused across multiple requests to the same origin.

// Simplified concept of browser connection pooling
class ConnectionPool {
  constructor(maxConnections = 6) {
    this.pool = new Map(); // origin -> connection[]
    this.maxConnections = maxConnections;
  }

  getConnection(origin) {
    if (!this.pool.has(origin)) {
      this.pool.set(origin, []);
    }

    const connections = this.pool.get(origin);
    if (connections.length > 0) {
      return connections.pop(); // Reuse existing connection
    }

    return this.createNewConnection(origin);
  }

  releaseConnection(origin, connection) {
    const connections = this.pool.get(origin);
    if (connections.length < this.maxConnections) {
      connections.push(connection);
    }
  }
}

HTTP/2 amplifies this behavior by using a single TCP connection for multiple simultaneous streams. A browser typically opens 6-10 HTTP/2 connections per domain, but the exact configuration varies between browsers and versions.

The Fingerprinting Mechanism

Connection Timing Signatures

Browsers exhibit unique connection establishment patterns based on:

// Example: Measuring connection fingerprint
function fingerprintConnection(url) {
  const timings = {
    dns: 0,
    tcp: 0,
    tls: 0,
    ttfb: 0 // time to first byte
  };

  const start = performance.now();

  fetch(url, { method: 'HEAD' })
    .then(() => {
      timings.total = performance.now() - start;
      // Send timings back for fingerprinting
      console.log('Connection fingerprint:', timings);
    });
}

Server Push Fingerprinting

HTTP/2 server push allows servers to preemptively send resources. However, push patterns reveal browser behavior:

# Server Push example
:method: GET
:path: /critical.js
host: example.com

# Browser's push promise handling varies by implementation
# The presence, absence, or timing of push promises
# creates a unique behavioral signature

Malicious sites can detect:

ALPN and Protocol Fingerprinting

Application-Layer Protocol Negotiation (ALPN) reveals which protocols your browser supports:

// Detecting protocol support through timing
const protocols = ['h2', 'http/1.1', 'http/1.0'];
const results = {};

protocols.forEach(protocol => {
  const start = performance.now();
  fetch(`https://example.com`, {
    protocol
  }).finally(() => {
    results[protocol] = performance.now() - start;
  });
});

The timing differences between successful and failed protocol negotiations create distinguishable patterns.

Real-World Fingerprinting Techniques

1. Connection Reuse Analysis

When you navigate between pages on the same domain, browsers reuse existing connections. Attackers can measure:

// Measuring connection reuse
const observedConnections = new Set();

function monitorConnections(entries) {
  entries.forEach(entry => {
    if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
      // Connection reuse creates specific timing signatures
      observedConnections.add(entry.name);
    }
  });
}

const observer = new PerformanceObserver(monitorConnections);
observer.observe({ entryTypes: ['resource'] });

2. HTTP/2 Settings Frame Analysis

HTTP/2 endpoints exchange SETTINGS frames during connection initialization. These settings include:

Different browsers send different default values, creating a browser identification vector:

Browser Max Concurrent Streams Initial Window Size
Chrome 100 65535
Firefox 128 65535
Safari 200 65535

3. Connection Pool Exhaustion

Fingerprinters can probe connection limits:

// Exhaustion-based fingerprinting
async function exhaustConnections(origin) {
  const connections = [];
  const start = performance.now();

  try {
    while (true) {
      connections.push(await fetch(origin + '?conn=' + connections.length));
    }
  } catch (e) {
    const limit = connections.length;
    const timing = performance.now() - start;
    return { limit, timing }; // Unique to browser
  }
}

The number of connections before failure, combined with timing data, produces a unique fingerprint.

Privacy Implications

This fingerprinting method is particularly dangerous because:

  1. Persistent across sessions: Connection characteristics remain stable
  2. Hard to block: Connection pooling is fundamental to browser architecture
  3. Works across domains: Using shared CDNs or embedded resources
  4. No cookies required: Identifies users without storing any state

HTTP/3 and QUIC: Does It Change the Threat?

HTTP/3 replaces TCP with QUIC (a UDP-based transport protocol), which changes—but does not eliminate—the connection fingerprinting surface. QUIC introduces connection IDs that can persist across IP address changes, creating a new tracking vector even when users switch networks.

Key QUIC fingerprinting signals include:

# Analyze QUIC traffic fingerprint using tshark
tshark -i eth0 -f "udp port 443" -Y "quic" \
  -T fields -e quic.version -e quic.connection_id_length \
  -e quic.initial_packet -w quic_capture.pcap

The practical takeaway: migrating to HTTP/3 does not provide a privacy improvement for fingerprinting resistance. It shifts the attack surface rather than eliminating it.

Cross-Origin Fingerprinting via Shared Resources

One often-overlooked vector involves shared CDN endpoints. When your browser loads a resource from a CDN shared across thousands of websites, the CDN operator can correlate connection reuse patterns to identify users across those sites without cookies.

This works because:

  1. Browser A visits site-one.com, which loads jQuery from cdn.example.com
  2. Browser A visits site-two.com, which also loads from cdn.example.com
  3. The CDN observes the same persistent connection being reused — identifying the same browser across two unrelated sites

The subresource integrity (SRI) mechanism prevents script tampering but does nothing to prevent this connection-level tracking.

<!-- SRI prevents script tampering but not connection fingerprinting -->
<script
  src="https://cdn.example.com/jquery-3.7.1.min.js"
  integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="
  crossorigin="anonymous">
</script>

Developers building privacy-sensitive applications should self-host critical scripts or use service workers to proxy CDN requests through the same origin.

Mitigation Strategies

For Users

For Developers

// Server-side: Avoid creating unique fingerprints
const http2 = require('http2');

const server = http2.createSecureServer({
  // Use standard settings across deployments
  settings: {
    maxConcurrentStreams: 100, // Consistent with common browsers
    initialWindowSize: 65535,
    maxFrameSize: 16384
  }
});

// Disable server push if not needed
server.on('stream', (stream, headers) => {
  // Don't push proactively
  stream.respond({ ':status': 200 });
});

Service Worker Proxy Pattern

A service worker can intercept all outgoing requests and route them through a single origin, reducing the connection diversity visible to external servers:

// service-worker.js
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  // Route all third-party requests through your own proxy
  if (url.hostname !== self.location.hostname) {
    const proxiedUrl = `/proxy?url=${encodeURIComponent(event.request.url)}`;
    event.respondWith(fetch(proxiedUrl));
  }
});

This approach consolidates connection pools, reducing the number of external endpoints that can observe your browser’s connection behavior.

For Site Operators

Testing Your Application’s Fingerprint Surface

Developers can audit their own application’s connection fingerprint exposure using browser developer tools and network analysis:

# Capture HTTP/2 frames using Wireshark
tshark -i lo -w capture.pcap -f "port 443"

# Then analyze SETTINGS frames
tshark -r capture.pcap -Y "http2.type == 4" \
  -T fields -e http2.settings.id -e http2.settings.value

The Mozilla Observatory and similar tools flag some connection-level issues, but dedicated fingerprinting test suites like coveryourtracks.eff.org provide more actionable data on your browser’s uniqueness.

Built by theluckystrike — More at zovo.one