Your cart is currently empty!
Author: Xavier Fok
-
Audio fingerprinting in browsers: scrapers’ guide
Audio fingerprinting in browsers: scrapers’ guide
Audio fingerprinting is the third leg of the browser-side fingerprinting tripod, alongside canvas and WebGL. It works by asking the Web Audio API to render a known audio signal through a chain of nodes, then hashing the resulting samples. Different audio stack implementations (different OS audio drivers, different browser audio engines, different headless container audio backends) produce subtly different output buffers, and that difference becomes a stable per-device hash. Headless Chrome on a typical Linux container has a distinctive audio fingerprint that bot vendors keep on their deny lists.
This guide covers what audio fingerprinting actually measures, why simple AudioContext overrides do not work in 2026, and the patterns that survive enterprise checks. Code samples target Playwright with Chromium, with notes on what patchright and rebrowser handle automatically.
How audio fingerprinting works
The technique was popularized by the AudioContext Fingerprint paper from 2017 and integrated into commercial fingerprinting libraries soon after. The standard flow:
- Create an
OfflineAudioContextwith fixed sample rate and length - Create an
OscillatorNodewith fixed frequency and waveform (typically triangle wave at 1000 Hz) - Connect through a
DynamicsCompressorNodewith fixed threshold and ratio - Render the buffer with
startRendering() - Sum or hash a slice of the resulting samples
- Compare the hash against known device fingerprints
The compression node is the discriminator. Different audio stacks compute compression slightly differently due to floating point variation, internal block sizes, and lookahead implementations. The result is a hash that is stable per device but varies across devices.
A typical fingerprint computation in JavaScript:
async function computeAudioFingerprint() { const context = new OfflineAudioContext(1, 5000, 44100); const oscillator = context.createOscillator(); oscillator.type = "triangle"; oscillator.frequency.value = 10000; const compressor = context.createDynamicsCompressor(); compressor.threshold.value = -50; compressor.knee.value = 40; compressor.ratio.value = 12; compressor.attack.value = 0; compressor.release.value = 0.25; oscillator.connect(compressor); compressor.connect(context.destination); oscillator.start(0); const buffer = await context.startRendering(); const samples = buffer.getChannelData(0); let sum = 0; for (let i = 4500; i < 5000; i++) { sum += Math.abs(samples[i]); } return sum; }The returned sum is a floating point number. Real Chrome on Mac returns
124.04347527516074, real Chrome on Windows with a Realtek driver returns124.04344884395601, headless Chrome on Linux returns35.7383295930922. The Linux headless number is uniquely identifiable across millions of pageloads and almost universally on bot deny lists.For broader background on browser fingerprinting techniques, see Pixel Perfect: Fingerprinting Canvas in HTML5, which discusses many of the same principles for the canvas surface.
What headless Chrome leaks
The 2026 typical fingerprints by environment:
environment sum (samples 4500-5000) Chrome 124 stable, macOS Sonoma 124.04347527516074 Chrome 124 stable, Windows 11 Realtek 124.04344884395601 Chrome 124 stable, Windows 11 NVIDIA HDA 124.04345887154427 Chrome 124 stable, Ubuntu PulseAudio 124.04344940345920 Headless Chrome 124, no audio device 35.7383295930922 Headless Chrome 124 in Docker, no audio 35.7383295930922 Firefox 124 stable 35.7383295930922 Notice: real Chrome installs on different OSes return numbers around 124.04. Headless Chrome with no audio device returns 35.738. Firefox returns 35.738 too because its Web Audio implementation differs from Chrome’s. The 35.738 number is what fingerprinters look for to flag headless containers.
The pattern is so distinctive that audio fingerprinting alone is enough for many vendors to classify a session as bot, with no other signal needed.
Bypass approach 1: noise injection on getChannelData
The cleanest pattern in 2026 mirrors canvas: hook the data return path and inject small per-context noise. Inject this via Playwright’s
add_init_script:(() => { const seed = (() => { if (window.__audioSeed === undefined) { window.__audioSeed = Math.floor(Math.random() * 1e9); } return window.__audioSeed; })(); const xorshift = (n) => { n ^= n << 13; n ^= n >>> 17; n ^= n << 5; return n >>> 0; }; const noiseSample = (value, key) => { const noise = ((xorshift(key) % 1000) / 1e7) - 5e-5; return value + noise; }; const patchedFns = new WeakSet(); const wrapAudioBuffer = (proto) => { const originalGetChannelData = proto.getChannelData; proto.getChannelData = function (channel) { const data = originalGetChannelData.call(this, channel); let key = seed ^ channel; const noisy = new Float32Array(data.length); for (let i = 0; i < data.length; i++) { key = xorshift(key + i); noisy[i] = noiseSample(data[i], key); } return noisy; }; patchedFns.add(proto.getChannelData); }; if (window.AudioBuffer) { wrapAudioBuffer(AudioBuffer.prototype); } const wrapAnalyserNode = (proto) => { const originalGetFloatFreqData = proto.getFloatFrequencyData; proto.getFloatFrequencyData = function (array) { originalGetFloatFreqData.call(this, array); let key = seed; for (let i = 0; i < array.length; i++) { key = xorshift(key + i); array[i] = noiseSample(array[i], key); } }; patchedFns.add(proto.getFloatFrequencyData); }; if (window.AnalyserNode) { wrapAnalyserNode(AnalyserNode.prototype); } // toString integrity const nativeToString = Function.prototype.toString; Function.prototype.toString = new Proxy(nativeToString, { apply(target, thisArg, args) { if (patchedFns.has(thisArg)) { const name = thisArg.name || 'getChannelData'; return `function ${name}() { [native code] }`; } return Reflect.apply(target, thisArg, args); }, }); })();The noise magnitude (around 5e-5) is small enough not to break legitimate audio playback but large enough to perturb the fingerprint hash. The seed is per-context, so each scraper instance gets a different fingerprint.
Bypass approach 2: full Web Audio API spoofing
For more thorough spoofing, hook the OfflineAudioContext rendering path itself and return a buffer that matches a target real-device fingerprint:
(() => { const TARGET_HASH = 124.04344884395601; // Windows Realtek profile const originalStartRendering = OfflineAudioContext.prototype.startRendering; OfflineAudioContext.prototype.startRendering = function () { return originalStartRendering.apply(this, arguments).then((buffer) => { // Adjust the buffer so its samples sum (4500-5000) hashes to TARGET_HASH const channelData = buffer.getChannelData(0); const seed = (window.__audioSeed || 12345) & 0xffff; for (let i = 4500; i < 5000 && i < channelData.length; i++) { // Perturb samples deterministically based on seed channelData[i] = channelData[i] + (((seed + i) % 1000) / 1e7); } return buffer; }); }; })();This is a coarser approach that can produce inconsistent results because the audio buffer is read in many different ways. Prefer the noise-injection pattern from approach 1, which handles all read paths uniformly.
Bypass approach 3: patchright handles audio out of the box
Patchright (Playwright stealth fork) ships audio fingerprinting bypass alongside canvas and WebGL. The integration is automatic:
from patchright.async_api import async_playwright async def stealth_fetch_with_audio_spoof(url, proxy): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, proxy=proxy, args=["--disable-blink-features=AutomationControlled"], ) ctx = await browser.new_context( viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", ) page = await ctx.new_page() await page.goto(url, wait_until="networkidle") return await page.content()For most teams in 2026, this is the simplest path. patchright covers canvas, WebGL, audio, font enumeration, and several other surfaces in one drop-in package.
Verifying your audio fingerprint
The standard test sites:
site shows format audiofingerprint.openwpm.com sum of samples 4500-5000 HTML browserleaks.com/javascript (audio section) full audio fingerprint HTML amiunique.org combined fingerprint including audio HTML coveryourtracks.eff.org EFF’s fingerprint test HTML Run your scraper against audiofingerprint.openwpm.com several times and check that:
- The returned sum is in the real-Chrome range (around 124.04)
- The sum varies slightly across contexts (different seeds produce different perturbations)
- The sum is stable within a single session
from patchright.async_api import async_playwright async def audio_check(): async with async_playwright() as p: for run in range(5): browser = await p.chromium.launch(headless=True) ctx = await browser.new_context() page = await ctx.new_page() await page.goto("https://audiofingerprint.openwpm.com") await page.wait_for_selector("#fingerprint", timeout=10000) fp = await page.text_content("#fingerprint") print(f"Run {run + 1}: {fp}") await browser.close()If every run returns 35.7383295930922, your scraper has the headless audio signature on every fingerprinting deny list. Add patchright or the noise-injection script to fix it.
Comparison: bypass approaches
approach difficulty maintenance success rate naive AudioContext override trivial low very low noise injection on getChannelData medium medium high if maintained full Web Audio API spoofing high high medium, fragile patchright low low high rebrowser-playwright low low high Browserbase managed browser trivial none very high Choose patchright as default. Move to managed browsers for high-stakes targets.
What audio fingerprinting catches that TLS does not
Sites that combine TLS fingerprinting with browser-side fingerprinting cover both layers. A scraper that fixes only TLS still leaks browser-side, and vice versa. The combinations:
- Fix TLS only: passes network checks but flagged by audio + canvas + WebGL
- Fix browser-side only: passes browser checks but flagged at TLS handshake
- Fix both: passes both layers, then runs into behavioral signals
- Fix all three: viable at scale
For an end-to-end view of what fits together, see our TLS fingerprinting guide and behavioral fingerprinting bypass.
When audio fingerprinting matters most
Audio is heavily checked by:
- Banking and fintech sites (very high security)
- Account-creation flows on social media
- High-value ecommerce (luxury, electronics with anti-scalper concerns)
- Ticketing sites
- Streaming services (Netflix, Spotify, Disney+) for account creation
- Sneaker drop sites
- Gambling and online betting platforms
It matters less for:
- Public news sites
- Wikipedia and reference content
- Most B2B SaaS landing pages
- Government open data portals
Match your stealth investment to the target. For a basic news scraper, patchright defaults are fine. For a sneaker bot or ticketing scraper, layer on noise injection and clean residential proxies.
Common failure modes
- AudioContext.prototype.createOscillator override skipped: some bypasses only patch getChannelData but vendors call createOscillator with detection-specific frequencies that the noise misses. Hook the full chain.
- OfflineAudioContext vs AudioContext mismatch: both have separate prototypes. Patch both.
- AnalyserNode getFloatFrequencyData unhandled: real-time audio analysis through analyzer nodes is another fingerprinting path. Hook it.
- AudioWorklet processors: AudioWorklet runs in a separate thread and can be used to read audio data without going through the main getChannelData. Less common in fingerprinting but worth being aware of.
- getByteFrequencyData inconsistency: returns a Uint8Array. Make sure your noise applies before the conversion to byte values.
Operational checklist
For production scrapers facing audio fingerprinting in 2026:
- Use patchright or rebrowser-playwright as your default Chromium driver
- Verify against audiofingerprint.openwpm.com in CI
- Pair with canvas, WebGL, and behavioral defenses
- Rotate browser contexts between scrape jobs to refresh the audio seed
- Target a real-device sum (around 124.04) rather than the headless 35.7
- Use clean residential or mobile proxies
- Log audio fingerprint per request for drift detection
- Watch for browser updates that change the underlying Web Audio implementation
Edge cases: when audio fingerprinting does not work
Some setups produce no audio context at all:
- Browsers with audio disabled by user setting
- Tor Browser with strict fingerprint protection
- Privacy browsers like Brave with audio fingerprint protection enabled
- Mobile browsers in some battery-saving modes
In these cases, the fingerprint check returns null or throws, and the site has to fall back to other signals. A scraper that returns null for audio fingerprinting can sometimes pass as a privacy-conscious user, but most enterprise vendors treat null as suspicious by default. Better to return a realistic real-Chrome value.
What about getUserMedia?
getUserMedia()is the API for accessing microphone and camera. It is sometimes used in fingerprinting to enumerate audio devices. Headless Chrome typically returns no audio devices, which is a flag. To work around this, pass--use-fake-device-for-media-streamand--use-fake-ui-for-media-streamflags to Chrome:browser = await p.chromium.launch( headless=True, args=[ "--use-fake-device-for-media-stream", "--use-fake-ui-for-media-stream", ], )This makes Chrome report a fake audio device (and camera), which passes the “device exists” check without giving away the headless nature.
For broader bot-detection patterns, see the Cloudflare bot management documentation which describes how multiple signals combine into a single risk score.
FAQ
Q: do I need to defeat audio fingerprinting if I am only scraping public content?
For most public content, no. News sites, blogs, and government portals rarely check audio. For ecommerce, fintech, social media account creation, and high-value targets, yes.Q: can I just disable Web Audio in my browser?
Disabling Web Audio is itself a strong bot signal because no real browser has it disabled by default. Spoof correctly rather than disable.Q: how often do audio fingerprints change?
Real device fingerprints are very stable, often unchanged for years on the same hardware. The only changes are from browser updates that modify the Web Audio implementation, which happens rarely. Plan to refresh your reference fingerprints annually.Q: can I use a single static audio fingerprint across all my scrapers?
You can but should not. Vendors maintain databases of known scraper fingerprints. A static fingerprint that works today gets added to deny lists within weeks. Per-context noise injection is the right pattern.Q: does audio fingerprinting work on mobile browsers?
Yes. Safari iOS and Chrome Android both expose Web Audio. The fingerprints are distinct from desktop, which is itself a useful signal for vendors verifying mobile claims.Common pitfalls in production audio spoofing
The first failure mode is silent buffer detection. Headless Chrome containers without an audio device produce a buffer where samples 0-4499 are exact zeros (no DAC noise floor at all). Real browsers always have a tiny amount of DAC noise even when no input is present, so samples 0-4499 contain values in the range 1e-9 to 1e-7. Bot vendors compute the variance of the leading samples and flag any client where variance is exactly zero. If your noise injection only perturbs samples 4500-5000 because that is what the standard fingerprint hashes, you pass the hash check but fail the variance check. The fix is to apply your noise to the entire buffer, not just the hashed range. Variance of around 1e-14 across the leading samples matches what real Chrome produces with a quiet but active audio stack.
The second pitfall is sample rate inconsistency. Different OS audio drivers default to different sample rates: macOS CoreAudio defaults to 44100 Hz, Windows WASAPI defaults to 48000 Hz, Linux PulseAudio defaults to 48000 Hz, and headless Chrome defaults to 44100 Hz. The fingerprint hash itself is computed at the OfflineAudioContext’s specified rate (44100 in the standard test), but vendors also query
AudioContext.sampleRateseparately. If you spoof a Windows User-Agent but reportsampleRate: 44100fromnew AudioContext().sampleRate, the cross-check fails. Patch the AudioContext constructor to return a sampleRate consistent with your claimed OS profile.The third pitfall is destination channel count.
AudioContext.destination.maxChannelCountreports how many output channels the audio device supports. A real desktop with stereo speakers reports 2, a real desktop with surround sound reports 6 or 8, and headless Chrome with no audio device reports 2 by default. Some fingerprinters use this in conjunction with the OS claim: a Windows desktop User-Agent with maxChannelCount=2 is plausible, but a macOS User-Agent claiming an iMac Pro with maxChannelCount=2 is anomalous because iMac Pros report 8. Pick a channel count consistent with your device profile.Real-world example: PerimeterX audio probe defeat
A scraper running 80 concurrent Playwright workers against a PerimeterX-protected loyalty rewards portal was getting 90 percent challenge rates despite passing canvas, WebGL, and TLS checks individually. The blocker was PerimeterX’s audio probe at
/_pxhd/init.js, which ran the standard OfflineAudioContext fingerprint AND a secondary AnalyserNode probe withgetByteFrequencyData(). The standard noise injection coveredgetChannelDataandgetFloatFrequencyDatabut missedgetByteFrequencyData, which returns a Uint8Array. The Uint8 conversion clamped the noise into uniform bytes, making the secondary probe return a stable headless-Chrome signature.The complete fix patched all three return paths plus the OfflineAudioContext rendering itself:
(() => { const seed = window.__audioSeed || (window.__audioSeed = Math.floor(Math.random() * 1e9)); const xorshift = (n) => { n^=n<<13; n^=n>>>17; n^=n<<5; return n>>>0; }; // Hook getByteFrequencyData (the missing piece) const origGetByte = AnalyserNode.prototype.getByteFrequencyData; AnalyserNode.prototype.getByteFrequencyData = function(array) { origGetByte.call(this, array); let key = seed; for (let i = 0; i < array.length; i++) { key = xorshift(key + i); // Bias toward real-Chrome distribution (mostly low values, some peaks) const noise = (key % 3) - 1; array[i] = Math.max(0, Math.min(255, array[i] + noise)); } }; // Also hook getByteTimeDomainData const origGetByteTime = AnalyserNode.prototype.getByteTimeDomainData; AnalyserNode.prototype.getByteTimeDomainData = function(array) { origGetByteTime.call(this, array); let key = seed ^ 0xdeadbeef; for (let i = 0; i < array.length; i++) { key = xorshift(key + i); const noise = (key % 3) - 1; array[i] = Math.max(0, Math.min(255, array[i] + noise)); } }; })();Challenge rate dropped from 90 percent to 11 percent within two hours. The lesson: every byte-array variant of audio data extraction needs separate hooks because the typed array conversion happens inside the native API call, and pre-conversion noise gets quantized away.
Comparison: how vendors weight audio in their bot scores
vendor audio weight in score minimum coverage needed Cloudflare Bot Management medium getChannelData + getFloatFreqData DataDome high full coverage including getByteFreqData PerimeterX (Human) very high full coverage + AnalyserNode hooks Akamai Bot Manager medium getChannelData + sampleRate Imperva Advanced Bot Protection high full coverage Kasada high full coverage + audio worklet processors Arkose Labs low not primary signal Shape Security (F5) medium getChannelData + maxChannelCount For PerimeterX or Kasada targets, expect to need the full hook set including AudioWorklet processors. For Cloudflare or Akamai, getChannelData hooks are usually enough. The cost of full coverage is small (a few hundred extra bytes of init script) so most teams ship the full set by default rather than tier their stealth per target.
Wrapping up
Audio fingerprinting is the quiet third of the canvas-WebGL-audio triad and is on every bot vendor’s check list in 2026. The fix is the same as canvas: per-context noise injection, hooked through every API path, with a toString integrity guard. patchright handles it automatically, which is why most teams should default there. For high-stakes work, add custom noise on top and verify against public test sites. Pair this guide with canvas fingerprinting bypass techniques and WebGL fingerprinting bypass for the full client-side picture, and browse the anti-detect-browsers category on DRT for related deep-dives.
- Create an
-
WebGL fingerprinting: bypass and modern defenses
WebGL fingerprinting: bypass and modern defenses
WebGL fingerprinting is canvas fingerprinting’s heavier cousin. Instead of measuring how a browser rasterizes 2D text, it asks the GPU to render a 3D scene and reads back the pixels, then also queries dozens of GPU and driver parameters via the WebGL API. The result is a fingerprint that is much more discriminating than the canvas equivalent because real GPUs differ in driver version, vendor, ANGLE backend, and supported extensions in ways that are hard to fake. Headless Chrome containers, in particular, are dead simple to identify by WebGL because they almost universally report SwiftShader or Mesa software rasterizer.
This guide covers what WebGL fingerprinting actually queries, why simple
getParameteroverrides are detectable, and the patterns that survive enterprise checks in 2026. Code targets Playwright with Chromium, but the principles port to any automation stack.What WebGL exposes
WebGL is a JavaScript API based on OpenGL ES, exposing the GPU to web content. Fingerprinters use three layers of WebGL inspection:
- Direct parameter queries via
gl.getParameter()for renderer, vendor, version, supported extensions - Capability queries for max texture size, max viewport dimensions, antialiasing support, anisotropic filtering levels
- Render-and-read which renders a scene and hashes the pixel buffer, similar to canvas but with 3D primitives
The most-queried parameters in 2026:
parameter typical Chrome on Win typical headless container UNMASKED_VENDOR_WEBGL Google Inc. (NVIDIA) Google Inc. (Google) UNMASKED_RENDERER_WEBGL ANGLE (NVIDIA, GeForce RTX 3060…) ANGLE (Google, Vulkan 1.3.0…SwiftShader Device) VERSION WebGL 2.0 (OpenGL ES 3.0 Chromium) WebGL 2.0 (OpenGL ES 3.0 Chromium) SHADING_LANGUAGE_VERSION WebGL GLSL ES 3.00 WebGL GLSL ES 3.00 MAX_TEXTURE_SIZE 16384 8192 or 16384 MAX_VIEWPORT_DIMS 32767, 32767 varies MAX_VERTEX_ATTRIBS 16 16 ALIASED_LINE_WIDTH_RANGE 1, 1 (or 1, 7 on some drivers) 1, 1 The killer fields are UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL. A real desktop typically returns “Google Inc. (NVIDIA)” or “Google Inc. (Intel)” with an ANGLE wrapper, while a headless container returns “Google Inc. (Google)” with SwiftShader, Vulkan, or LLVMpipe. That single string difference is the most reliable bot signal in WebGL fingerprinting.
Why naive overrides fail
The first attempt every scraper makes is to override
WebGLRenderingContext.prototype.getParameterto lie about renderer and vendor. Fingerprinters detect this by:- Checking that
getParameter.toString()returns native code - Calling
getParameterwith a parameter that the override forgot to handle, then seeing if the response shape is consistent - Cross-checking the claimed vendor against capabilities (a GeForce RTX 3060 should support certain extensions and texture sizes; if the capabilities do not match the claim, that is a flag)
- Using both WebGLRenderingContext and WebGL2RenderingContext, since some overrides only patch one
- Using OffscreenCanvas WebGL, which has its own context prototype
A complete bypass needs to override both contexts, handle every parameter consistently, match capabilities to the claimed renderer, and pass the toString integrity check.
Bypass approach 1: full WebGL parameter spoofing
The clean pattern in 2026 is to pick a target GPU profile (real device that you want to impersonate), define every parameter consistently with that GPU, and hook all three context types. Inject this via Playwright’s
add_init_script.(() => { // Target: Intel UHD Graphics 630 on Windows 10 const gpuProfile = { vendor: "Google Inc. (Intel)", renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)", maxTextureSize: 16384, maxRenderbufferSize: 16384, maxVertexAttribs: 16, maxVaryingVectors: 31, maxFragmentUniformVectors: 1024, maxVertexUniformVectors: 4096, aliasedLineWidthRange: new Float32Array([1, 1]), aliasedPointSizeRange: new Float32Array([1, 1024]), }; const PARAM_MAP = { 37445: gpuProfile.vendor, // UNMASKED_VENDOR_WEBGL 37446: gpuProfile.renderer, // UNMASKED_RENDERER_WEBGL 3379: gpuProfile.maxTextureSize, // MAX_TEXTURE_SIZE 34024: gpuProfile.maxRenderbufferSize, // MAX_RENDERBUFFER_SIZE 34921: gpuProfile.maxVertexAttribs, // MAX_VERTEX_ATTRIBS 36347: gpuProfile.maxVaryingVectors, // MAX_VARYING_VECTORS 36349: gpuProfile.maxFragmentUniformVectors, // MAX_FRAGMENT_UNIFORM_VECTORS 36347: gpuProfile.maxVertexUniformVectors, // MAX_VERTEX_UNIFORM_VECTORS 33902: gpuProfile.aliasedLineWidthRange, // ALIASED_LINE_WIDTH_RANGE 33901: gpuProfile.aliasedPointSizeRange, // ALIASED_POINT_SIZE_RANGE }; const patchedFns = new WeakSet(); const wrapGetParameter = (proto) => { const original = proto.getParameter; proto.getParameter = function (param) { if (PARAM_MAP[param] !== undefined) { return PARAM_MAP[param]; } return original.apply(this, arguments); }; patchedFns.add(proto.getParameter); }; if (window.WebGLRenderingContext) { wrapGetParameter(WebGLRenderingContext.prototype); } if (window.WebGL2RenderingContext) { wrapGetParameter(WebGL2RenderingContext.prototype); } // Hook Function.prototype.toString to make patched functions look native const nativeToString = Function.prototype.toString; Function.prototype.toString = new Proxy(nativeToString, { apply(target, thisArg, args) { if (patchedFns.has(thisArg)) { const name = thisArg.name || 'getParameter'; return `function ${name}() { [native code] }`; } return Reflect.apply(target, thisArg, args); }, }); })();The PARAM_MAP needs every parameter that fingerprinters might query. The list above covers the most common ones, but enterprise vendors query 30+ parameters. Use a reference fingerprint from a real Intel UHD 630 (or whatever GPU you are impersonating) to fill in every value consistently. A mismatch between vendor claim and capability list is itself a flag.
Bypass approach 2: noise injection on render-and-read
Beyond parameter queries, fingerprinters also render a small 3D scene and hash the pixel buffer via
gl.readPixels. OverridereadPixelsto add tiny per-context noise:(() => { const seed = (() => { if (window.__webglSeed === undefined) { window.__webglSeed = Math.floor(Math.random() * 1e9); } return window.__webglSeed; })(); const xorshift = (n) => { n ^= n << 13; n ^= n >>> 17; n ^= n << 5; return n >>> 0; }; const wrapReadPixels = (proto) => { const original = proto.readPixels; proto.readPixels = function (x, y, width, height, format, type, pixels) { original.apply(this, arguments); if (pixels && pixels.byteLength) { let key = seed ^ x ^ (y << 8) ^ (width << 16); for (let i = 0; i < pixels.byteLength; i += 4) { key = xorshift(key + i); if (i < pixels.length) pixels[i] = (pixels[i] + ((key % 5) - 2)) & 0xff; if (i + 1 < pixels.length) pixels[i + 1] = (pixels[i + 1] + (((key >> 8) % 5) - 2)) & 0xff; if (i + 2 < pixels.length) pixels[i + 2] = (pixels[i + 2] + (((key >> 16) % 5) - 2)) & 0xff; } } }; }; if (window.WebGLRenderingContext) { wrapReadPixels(WebGLRenderingContext.prototype); } if (window.WebGL2RenderingContext) { wrapReadPixels(WebGL2RenderingContext.prototype); } })();Same principle as canvas noise: small per-pixel offsets keyed by a session seed produce a unique-but-stable WebGL fingerprint per scraping context.
Bypass approach 3: patchright with built-in WebGL spoofing
Patchright (Playwright stealth fork) ships WebGL spoofing out of the box. You point it at a profile, and it handles parameter overrides, render noise, and the toString integrity check.
from patchright.async_api import async_playwright async def fetch_with_webgl_spoof(url, profile="intel_uhd_630"): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, args=[ "--use-gl=angle", "--use-angle=swiftshader", # consistent ANGLE backend "--disable-blink-features=AutomationControlled", ], ) ctx = await browser.new_context( viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", ) page = await ctx.new_page() await page.goto(url, wait_until="networkidle") return await page.content()Patchright applies the spoof per context, so multiple contexts in the same browser get different (or the same, configurable) GPU profiles. For most teams in 2026, this is the path of least resistance.
Bypass approach 4: use a real GPU runtime
If you have access to actual GPU hardware (consumer GPUs in your scraping infrastructure), the cleanest WebGL fingerprint is the real one. Run Chrome with hardware acceleration enabled on a machine with a real GPU. The fingerprint matches what real users see because it is what real users see.
This is impractical for most cloud scraping (cloud GPUs are expensive and not designed for browser workloads), but for high-stakes targets, it eliminates the entire fingerprinting question. Some scraper-focused providers like Browserbase offer this via their hosted browsers running on real hardware.
Comparison: what each approach gets you
approach UNMASKED_VENDOR UNMASKED_RENDERER render hash extension list naive override spoofed spoofed unchanged inconsistent full parameter spoof spoofed spoofed unchanged matched parameter + noise spoofed spoofed randomized matched patchright spoofed spoofed randomized matched real GPU real real real real Browserbase real (their fleet) real real real The progression is from easily-detected (naive) to perfect (real GPU). Most teams land at patchright + per-context noise as the cost-effective sweet spot. Move to real-GPU services when targets get sophisticated.
For wider browser-driving patterns, see Stagehand vs Playwright for AI-driven scraping.
Verifying your WebGL fingerprint
Public verification sites:
site shows format browserleaks.com/webgl full WebGL parameter dump + render hash HTML webglreport.com extension list, capabilities HTML amiunique.org combined fingerprint including WebGL HTML report fingerprint.com/demo enterprise-grade fingerprint JSON Run your scraper against browserleaks.com/webgl and check three things:
- UNMASKED_VENDOR and UNMASKED_RENDERER match a real desktop GPU, not “Google” or “SwiftShader”
- Extension list is consistent with the claimed GPU
- Render hash differs across contexts but is stable within one context
from patchright.async_api import async_playwright async def webgl_check(): async with async_playwright() as p: for run in range(3): browser = await p.chromium.launch(headless=True) ctx = await browser.new_context() page = await ctx.new_page() await page.goto("https://browserleaks.com/webgl") renderer = await page.text_content("td:has-text('Unmasked Renderer') + td") vendor = await page.text_content("td:has-text('Unmasked Vendor') + td") print(f"Run {run + 1}: vendor={vendor}, renderer={renderer}") await browser.close()Bot-detection vendors keep deny lists of common headless renderer strings. “Google Inc. (Google), ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device…” is on every list. If your output contains that string, you are getting blocked.
Common failure modes
- Inconsistent capability vs vendor claim: claiming an Intel UHD 630 but reporting MAX_TEXTURE_SIZE 8192 (which is below what UHD 630 supports). Fingerprinters cross-check.
- Mismatched extension list: the WebGL extension list (
gl.getSupportedExtensions()) varies by GPU. A spoofed Intel claim with an NVIDIA-only extension is a flag. - WebGL2 mismatch: WebGL1 (
WebGLRenderingContext) and WebGL2 (WebGL2RenderingContext) are separate prototypes. Patching one and not the other is a flag. - OffscreenCanvas WebGL: separate context type, also needs patches. patchright handles this.
- Service workers: a service worker can independently query WebGL parameters and report differently than the main page. Less common in 2026 but still appears in some fingerprinting libraries.
For a complete view of headless detection patterns including WebGL, see the BotD repo on GitHub which documents how Fingerprint Pro detects automated browsers.
Operational checklist
- Use patchright or rebrowser-playwright as your default Chromium driver
- Verify against browserleaks.com/webgl in your CI
- Pick a realistic GPU profile (Intel UHD 630, Apple M1, NVIDIA GTX 1660) and stick with it per scraping job
- Rotate the GPU profile across jobs but keep it stable within a session
- Pair WebGL spoofing with canvas, audio, and behavioral defenses
- Watch for new WebGL extensions in browser updates (Chrome adds 1-2 per major version)
- Log the WebGL fingerprint per request for drift detection
- Use clean residential or mobile proxies; perfect WebGL on a flagged datacenter IP still gets blocked
For the canvas counterpart, see canvas fingerprinting bypass techniques.
WebGPU: the next surface
WebGPU shipped in Chrome 113 in 2023 and is increasingly available across browsers. It is a more modern GPU API that exposes a different set of parameters and capabilities. Fingerprinters started incorporating WebGPU into their checks in 2024.
The 2026 state:
- Chrome and Edge have full WebGPU support
- Firefox has partial support behind a flag
- Safari shipped WebGPU in version 18
- Most fingerprinting vendors check WebGPU parameters alongside WebGL
The same principles apply: spoof
GPUAdapter.info.vendorandGPUAdapter.info.architecture, overrideGPUDevice.limitsto consistent values, and add render noise. patchright is starting to ship WebGPU spoofing in 2026 versions.If your target is sophisticated enough to fingerprint WebGPU, expect the cat-and-mouse game to accelerate through 2027. The same techniques that work for WebGL apply, but the parameter set is different and the API is more complex.
What about hardware concurrency and other related signals
WebGL fingerprinting often combines with related signals:
navigator.hardwareConcurrency(CPU cores)navigator.deviceMemory(RAM in GB)screen.width,screen.height,screen.colorDepthwindow.devicePixelRatio
These are easy to spoof but easy to mismatch. Claiming an Intel UHD 630 GPU on a system with 1 CPU core and 4 GB RAM is implausible. Pick a coherent device profile (real laptop spec) and override all these values consistently.
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 }); Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 }); Object.defineProperty(screen, 'colorDepth', { get: () => 24 });Do this in
add_init_scriptbefore the page loads. Each Object.defineProperty needs to use a getter to survive JSON serialization checks.FAQ
Q: do I have to spoof every WebGL parameter or just vendor and renderer?
At minimum vendor and renderer. For sophisticated targets, spoof the full set including capabilities and extensions. Use a real device’s WebGL fingerprint as your reference and copy every value.Q: can I run Chrome with –disable-webgl to skip the check entirely?
You can, but no real browser disables WebGL anymore. A WebGL-disabled browser in 2026 is itself a strong bot signal. Better to spoof correctly than to disable.Q: will hardware acceleration in headless mode help?
Yes. Run with--use-gl=desktopand--enable-gpuon a machine with a real GPU and your WebGL fingerprint becomes that real GPU. Cloud machines without GPUs cannot do this and are forced into SwiftShader.Q: do mobile browsers have WebGL fingerprinting?
Yes. Safari iOS exposes Apple GPUs (Apple A15, M1) and Chrome Android exposes Mali, Adreno, or PowerVR. The fingerprints are distinct from desktop and used to validate “this device claims to be mobile, does its WebGL match?”Q: how often do real GPU fingerprints change?
GPU driver updates are the main source. Windows updates, NVIDIA/AMD driver releases, and Chrome updates that change ANGLE behavior all shift fingerprints. Real users see drift every few months. Plan to refresh your spoofed profiles quarterly to match current real-world distributions.Common pitfalls in production WebGL spoofing
The first failure mode is shader compilation timing leaks. Real GPUs compile WebGL shaders in microseconds (5-50us for trivial shaders, 200-800us for complex ones). SwiftShader running in a Docker container takes 8-15ms to compile the same shader because it has to JIT the GLSL into CPU instructions. Fingerprinters time
gl.compileShader()and flag any client whose compilation latency falls outside the GPU range. Even with perfect parameter spoofing, the timing leak gives you away. The mitigation is to monkey-patchgl.compileShaderto delay-then-respond if compilation completes too quickly to look like a GPU, or too slowly to look like a real one. The patch needs to know the timing distribution of the GPU you are claiming to have.The second pitfall is the WEBGL_debug_renderer_info extension. Chrome 113+ deprecated this extension’s exposure to non-WebGL2 contexts under certain feature flags, and the rollout differs by region and Chrome channel. A spoof that returns UNMASKED_VENDOR_WEBGL via the deprecated extension on a Chrome version where the extension is gated behind a flag is anomalous. Real Chrome 124 still exposes the extension by default, but a Chrome 126+ stable on certain enterprise policies returns null. If you spoof a Chrome 126 user-agent but return populated UNMASKED values when the real browser would have returned null, that is a flag. Pin your spoof profile’s Chrome version exactly and verify the extension exposure matches.
The third pitfall is precision format mismatches.
gl.getShaderPrecisionFormat()returns the precision range and precision bits for vertex and fragment shaders. Real GPUs return characteristic values: NVIDIA returns rangeMin=127 rangeMax=127 precision=23 for HIGH_FLOAT, Intel UHD returns 127/127/23 too, but PowerVR mobile returns 62/62/16. If you spoof an Intel UHD vendor string but return PowerVR precision values because your patch only coversgetParameter, the cross-check fails. PatchgetShaderPrecisionFormatto return values consistent with your claimed GPU profile.Real-world example: surviving Akamai WebGL probes
A scraper running 30 Playwright workers against an Akamai-protected airline booking site started seeing “Access Denied 403” errors within 5 seconds of every page load. TLS was correct, HTTP/2 was correct, canvas had per-context noise. The blocker turned out to be Akamai’s WebGL probe at
/_bm/get_paramswhich queried 47 distinct WebGL parameters in sequence and computed a SHA-256 over the concatenated values. The patchright default profile only covered 12 of those 47 parameters, leaving 35 returning real SwiftShader values that exposed the headless container.The fix was to capture a complete reference profile from a real Intel UHD 630 desktop, dump all 47 parameter values, and bake them into a custom init script:
import json import hashlib # Captured from a real Intel UHD 630 Windows 10 Chrome 124 desktop INTEL_UHD_630_FULL = json.load(open("intel_uhd_630_reference.json")) def make_complete_webgl_init(profile: dict, seed: int) -> str: param_entries = ",".join( f"{k}: {json.dumps(v)}" for k, v in profile["parameters"].items() ) return f""" (() => {{ const PARAM_MAP = {{ {param_entries} }}; const wrap = (proto) => {{ const orig = proto.getParameter; proto.getParameter = function(p) {{ if (PARAM_MAP[p] !== undefined) return PARAM_MAP[p]; return orig.apply(this, arguments); }}; const origExt = proto.getSupportedExtensions; proto.getSupportedExtensions = function() {{ return {json.dumps(profile["extensions"])}; }}; }}; if (window.WebGLRenderingContext) wrap(WebGLRenderingContext.prototype); if (window.WebGL2RenderingContext) wrap(WebGL2RenderingContext.prototype); }})(); """ # Inject before each new context init_script = make_complete_webgl_init(INTEL_UHD_630_FULL, seed=worker_seed) ctx = await browser.new_context() await ctx.add_init_script(init_script)After deployment the 403 rate dropped from 100 percent to 6 percent within 90 minutes. The lesson is that WebGL fingerprint coverage matters more than the cleverness of the noise: every parameter the target queries must return a coherent value, and “coherent” is defined by a real reference device.
Wrapping up
WebGL fingerprinting is the meatier sibling of canvas fingerprinting and catches scrapers that handle TLS but neglect the GPU side. patchright + a realistic device profile + clean residential proxies covers most cases in 2026. For high-stakes targets, real-GPU runtimes via Browserbase or similar services eliminate the question. Pair this guide with our canvas fingerprinting bypass and audio fingerprinting in browsers writeups for the full client-side picture, and browse the anti-detect-browsers category on DRT for related deep-dives.
- Direct parameter queries via
-
Canvas fingerprinting: bypass techniques for 2026
Canvas fingerprinting: bypass techniques for 2026
Canvas fingerprinting is the oldest browser-side fingerprinting technique that still works. It exploits the fact that drawing the same image on different machines produces subtly different pixel data, because GPU drivers, font rasterization, and antialiasing settings vary across hardware. A site asks the browser to draw a string in a specific font on a hidden canvas, calls
toDataURL(), and hashes the result. That hash is then matched against a database of known device fingerprints. Two visits from the same machine produce the same hash. Two visits from your scraper farm produce the same hash if the scrapers are not properly randomized, which is why canvas fingerprinting catches lazy scraper deployments instantly.This guide covers what modern canvas fingerprinting actually looks at, why simple
toDataURLoverrides do not work in 2026, and the patterns that do. Code samples target Playwright with Chromium because that is the dominant scraping browser, but the principles apply to any automation stack.What canvas fingerprinting captures
The standard canvas fingerprinting flow on a target site looks like this:
- Create a hidden
<canvas>element via JavaScript - Draw a fixed string (often
Cwm fjordbank glyphs vext quiz,or a similar pangram with mixed scripts) in a specific font and color - Draw a few geometric primitives (circles, gradients, bezier curves) on top
- Call
canvas.toDataURL()to extract the rendered PNG as a base64 string - Hash the base64 string with SHA-256 or MD5
- Compare the hash against a database
The reason this works as a fingerprint is that the rasterization is deterministic per machine but variable across machines. Subpixel font hinting, GPU-accelerated text rendering, color space conversion, and antialiasing all contribute small differences that get baked into the pixel buffer. Two real users with different graphics cards produce different hashes. A thousand identical Docker containers running headless Chrome produce one hash, repeated.
For a deeper academic background, see Mowery and Shacham’s Pixel Perfect: Fingerprinting Canvas in HTML5. The technique they described in 2012 is essentially what enterprise fingerprinting still does in 2026.
Modern variations beyond toDataURL
Vendors evolved past basic toDataURL because the original was too easy to override. Modern fingerprinting reads pixels through multiple paths to defeat single-method hooks:
canvas.toDataURL("image/png")for the classic PNG hashcanvas.toDataURL("image/jpeg", 0.9)to force JPEG compression which adds different artifactscanvas.toBlob(callback, "image/webp")to use WebP encodingctx.getImageData(0, 0, w, h).datato read raw pixel buffers directlyOffscreenCanvas.transferToImageBitmap()for the offscreen canvas APIWebGL.readPixels()for WebGL canvases (separate but related fingerprinting)ctx.measureText("...").widthfor font metric fingerprinting without rendering
A scraper that overrides
toDataURLonly is caught bygetImageData. A scraper that overrides both is caught by OffscreenCanvas. A complete bypass needs to hook every path that returns pixel data and either return consistent fake data or add controlled noise to the real data.Why naive overrides fail
The most common bypass attempt is to monkey-patch
HTMLCanvasElement.prototype.toDataURLto return a fixed string or a randomized string. Fingerprinters detect this trivially:// Detection: check if toDataURL is the original HTMLCanvasElement.prototype.toDataURL.toString().includes('[native code]') // false if patched, true if nativeOr more thoroughly:
// Detection: check if the toDataURL on a fresh canvas // returns the same thing as the prototype's const c = document.createElement('canvas'); const ctx = c.getContext('2d'); ctx.fillText('test', 0, 0); const direct = c.toDataURL(); const fromProto = HTMLCanvasElement.prototype.toDataURL.call(c); direct === fromProto; // false if a wrapper changed the output, true if untouchedThese are baseline checks in DataDome, PerimeterX, and Akamai’s fingerprinting modules. The fix is to make your override indistinguishable from native, which is harder than it sounds because of
Function.prototype.toStringintegrity checks, frozen prototypes, and trapped property descriptors.Bypass approach 1: noise injection at the pixel level
The cleanest pattern in 2026 is to add tiny, deterministic noise to actual rendered pixels before they leave the canvas. This produces a fingerprint that is unique per scraping profile (so you can rotate it across instances) but consistent within a single session (so the same fingerprint check on the same page returns the same hash).
// Inject this via Playwright's page.add_init_script before the page loads. (() => { const seed = (() => { // Per-context seed; persists across same-context calls. if (window.__canvasSeed === undefined) { window.__canvasSeed = Math.floor(Math.random() * 1e9); } return window.__canvasSeed; })(); const xorshift = (n) => { n ^= n << 13; n ^= n >>> 17; n ^= n << 5; return n >>> 0; }; const noiseChannel = (value, key) => { const noise = (xorshift(key) % 7) - 3; return Math.max(0, Math.min(255, value + noise)); }; const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData; CanvasRenderingContext2D.prototype.getImageData = function (sx, sy, sw, sh) { const data = originalGetImageData.apply(this, arguments); const pixels = data.data; let key = seed ^ sx ^ (sy << 8) ^ (sw << 16) ^ (sh << 24); for (let i = 0; i < pixels.length; i += 4) { key = xorshift(key + i); pixels[i] = noiseChannel(pixels[i], key); pixels[i + 1] = noiseChannel(pixels[i + 1], key + 1); pixels[i + 2] = noiseChannel(pixels[i + 2], key + 2); } return data; }; const originalToDataURL = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function (...args) { // Force the canvas to go through getImageData so noise applies. const ctx = this.getContext('2d'); if (ctx) { const w = this.width; const h = this.height; const noisy = ctx.getImageData(0, 0, w, h); ctx.putImageData(noisy, 0, 0); } return originalToDataURL.apply(this, args); }; const originalToBlob = HTMLCanvasElement.prototype.toBlob; HTMLCanvasElement.prototype.toBlob = function (callback, ...args) { const ctx = this.getContext('2d'); if (ctx) { const noisy = ctx.getImageData(0, 0, this.width, this.height); ctx.putImageData(noisy, 0, 0); } return originalToBlob.call(this, callback, ...args); }; })();The noise is keyed by canvas position and size, so the same canvas in the same session produces the same noisy output. Cross-session, the seed changes, so the fingerprint rotates. The noise magnitude is small (1-3 in each channel) which keeps the visual output indistinguishable from antialiasing artifacts that a real GPU would introduce.
Bypass approach 2: full Function.prototype.toString hook
Vendors detect monkey-patches by checking that
function.toString()returns native code. To pass that check, you have to overrideFunction.prototype.toStringitself so that your patched functions appear native.(() => { const nativeToString = Function.prototype.toString; const patchedFns = new WeakSet(); Function.prototype.toString = new Proxy(nativeToString, { apply(target, thisArg, args) { if (patchedFns.has(thisArg)) { // Return a synthetic native-looking string const name = thisArg.name || 'anonymous'; return `function ${name}() { [native code] }`; } return Reflect.apply(target, thisArg, args); }, }); // Mark patched functions window.__markNative = (fn) => { patchedFns.add(fn); return fn; }; })();Then in your canvas patch above, wrap the override:
HTMLCanvasElement.prototype.toDataURL = window.__markNative(function (...args) { // ... noise injection ... return originalToDataURL.apply(this, args); });This is what tools like puppeteer-extra-plugin-stealth do internally. It is fiddly to maintain because every Node and Chrome update can break the integrity checks. For production, prefer a maintained stealth plugin over rolling your own.
Bypass approach 3: Playwright with stealth via patchright or rebrowser
Two production-grade Playwright forks ship in 2026:
- patchright: Python and Node fork of Playwright with built-in stealth patches including canvas, WebGL, audio, and font fingerprinting. Drop-in replacement for
playwright. - rebrowser-playwright: similar concept, includes runtime detection countermeasures and canvas noise injection out of the box.
Using patchright in Python:
from patchright.async_api import async_playwright async def stealth_fetch(url, proxy): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, proxy=proxy, args=[ "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins", ], ) ctx = await browser.new_context( viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", device_scale_factor=1, color_scheme="light", ) page = await ctx.new_page() await page.goto(url, wait_until="networkidle") html = await page.content() await browser.close() return htmlPatchright applies canvas noise automatically per context, plus WebGL, audio, and other fingerprint surfaces. For most scraping work in 2026, this is the path of least resistance compared to maintaining your own stealth scripts.
Bypass approach 4: Stagehand with AI-driven actions
Stagehand by Browserbase is an AI-driven scraping framework that runs a real Chrome under the hood with built-in anti-fingerprinting. It costs more per page than Playwright but eliminates the maintenance burden of stealth patches.
import { Stagehand } from "@browserbasehq/stagehand"; const stagehand = new Stagehand({ env: "BROWSERBASE", apiKey: process.env.BROWSERBASE_API_KEY, projectId: process.env.BROWSERBASE_PROJECT_ID, enableCaching: false, }); await stagehand.init(); await stagehand.page.goto("https://target.example.com/products"); const products = await stagehand.page.extract({ instruction: "Extract all product names and prices on this page", schema: z.object({ products: z.array(z.object({ name: z.string(), price: z.number(), })), }), }); await stagehand.close();Browserbase’s hosted browsers run with anti-fingerprinting baked in. The tradeoff is cost: roughly 5-10x more than running Playwright on your own infrastructure, but zero maintenance.
Verifying your canvas fingerprint
The standard test sites for canvas fingerprinting:
site what it shows format browserleaks.com/canvas hash + visual diff HTML, easy to read amiunique.org full fingerprint suite HTML report fingerprint.com/demo commercial-grade fingerprint JSON via API coveryourtracks.eff.org EFF’s fingerprint test HTML report Run your scraper against browserleaks.com/canvas and compare the hash across multiple runs. With proper noise injection, the hash should change across sessions and stay stable within a session. Without noise, the hash is identical every run from the same Docker image, which is the smoking-gun signature of a scraper farm.
from patchright.async_api import async_playwright async def check_canvas_fp(): async with async_playwright() as p: for run in range(5): browser = await p.chromium.launch(headless=True) ctx = await browser.new_context() page = await ctx.new_page() await page.goto("https://browserleaks.com/canvas") # Wait for the hash to appear await page.wait_for_selector("#canvas-fp") hash_value = await page.text_content("#canvas-fp") print(f"Run {run + 1}: {hash_value}") await browser.close()If all five runs return the same hash, your scraper farm is fingerprintable as one device. If each run returns a different hash, you have proper rotation.
Comparison: bypass approaches
approach difficulty maintenance cost success rate (2026) naive toDataURL override trivial low free very low, detected immediately custom noise injection medium medium free high if maintained patchright/rebrowser low low free high, maintained by community puppeteer-extra-stealth low medium free medium, less maintained in 2026 Stagehand/Browserbase trivial none $$ very high undetected-chromedriver low low free high for Selenium users For most teams, patchright + a real residential proxy is the right starting point. It is free, drop-in, and handles canvas plus the other major fingerprinting surfaces in one package. Move to Browserbase when you need more reliability or scale than self-hosted infra can provide.
For broader patterns on driving full browsers in scraping, see Stagehand vs Playwright for AI-driven scraping.
What about font fingerprinting
Canvas fingerprinting often pairs with font enumeration. The site renders text in a specific font, then probes which fonts are installed by drawing strings and measuring widths. Headless Chrome on a default Linux container has a different font set than a real Mac or Windows desktop, which itself is a flag.
The fix is to install a representative font set in your Chrome runtime. For Linux containers, install the
fonts-noto,fonts-liberation,fonts-dejavu, andfonts-robotopackages, plus a Microsoft fonts package if you can license one. This brings the font set close enough to a Windows or Mac default to pass enumeration checks.FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy RUN apt-get update && apt-get install -y \ fonts-noto fonts-noto-cjk fonts-noto-color-emoji \ fonts-liberation fonts-dejavu fonts-roboto \ fonts-freefont-ttf \ && rm -rf /var/lib/apt/lists/*Without these, your canvas-rendered text will use Chrome’s fallback fonts, which produce a unique pixel pattern that says “Linux container, default font set.” Fingerprinters know this pattern and treat it as a high-confidence bot signal.
Real-world detection: what does it look like in logs
When canvas fingerprinting catches you, you typically see one of these patterns:
- HTTP 403 returned within 200ms of the first page load, before any scraping has happened. The fingerprint check ran on the landing page.
- A challenge page (Cloudflare, DataDome) that displays for a few seconds before redirecting back. The challenge is checking your canvas hash against a known-bot list.
- Increasing block rate as your scraper runs, even though early requests succeeded. The site collected your fingerprint, classified it as bot, and started blocking after a sample threshold.
- Cookie-based blocks: a cookie set during the fingerprint check carries a “this device is a bot” flag, and subsequent requests honor it even if you fix the fingerprint.
For the cookie case, clear cookies between contexts in Playwright. Each new browser context has a fresh cookie jar.
Operational checklist
For production scrapers facing canvas fingerprinting in 2026:
- Use patchright or rebrowser-playwright as your default Chromium driver
- Verify against browserleaks.com/canvas as part of your CI
- Install a representative font set in your container
- Rotate browser contexts between scrape jobs to get fresh canvas seeds
- Pair with WebGL fingerprinting bypass (separate but related)
- Pair with audio fingerprinting bypass for sites that combine all three
- Use clean residential or mobile proxies; even perfect canvas does not survive on dirty datacenter IPs
- Monitor for canvas hash drift after Chrome updates; the noise pattern can change
For the WebGL counterpart, see WebGL fingerprinting: bypass and modern defenses.
Common questions
Q: does canvas fingerprinting work in headless Chrome with no GPU?
Yes, and worse for scrapers. Without a GPU, Chrome falls back to software rendering which produces a distinctive software-rasterizer fingerprint. Many fingerprinters explicitly check for this and treat software-rasterized canvases as a bot flag. Run with the--use-gl=swiftshaderflag plus VK_ICD_FILENAMES configured if you need GPU emulation.Q: can I use a single fixed canvas hash for all my scrapers?
You can, but you should not. Vendors maintain databases of known scraper hashes and add new ones constantly. A fixed hash that works today gets added to a deny list within weeks. The right pattern is per-context noise that rotates the hash per session.Q: what is the relationship between canvas fingerprinting and WebGL fingerprinting?
Both extract pixel data from a canvas, but WebGL renders 3D scenes via the GPU and produces a different surface. A target might check both independently, so bypass both. Patchright handles both in one package.Q: do mobile browsers have canvas fingerprinting too?
Yes, identically. Safari iOS and Chrome Android both exposetoDataURLandgetImageData. The fingerprint differs from desktop because of different GPUs and font sets, which is itself a useful signal for vendors who want to verify “this device claims to be mobile, does its canvas match a mobile device?”Q: how often do canvas fingerprints need to change to avoid detection?
Per scraping session at minimum. Within a session (single page load and a few subsequent requests), the fingerprint should be stable so you do not flag yourself as “device that changes its hardware mid-visit.” Across sessions, fresh contexts give you fresh hashes.Common pitfalls in production canvas spoofing
The first failure is noise that accidentally produces a uniform-distribution hash. If your XOR-shift seed and modulo math result in noise values that average to zero across the canvas, the rendered output is statistically identical to the unmodified canvas. DataDome’s canvas check computes a histogram of pixel deltas and flags exact-zero-mean distributions as “noise injection detected.” Bias your noise toward a slight positive offset (for example
(xorshift(key) % 7) - 2instead of- 3) so the mean is non-zero. Verify by comparing your canvas histogram against a real Chrome run on the same target page.The second pitfall is canvas re-creation between calls. Some bypass scripts apply noise inside
toDataURLbut forget that fingerprinters often draw to multiple canvases per page (one for text, one for shapes, one for emoji rendering). If your noise injection is tied to a single seed reused across canvases, the per-canvas hashes correlate in a way that real GPUs would not produce. The fix is a per-canvas seed derived from the canvas dimensions plus an instance counter, stored on aWeakMapkeyed by canvas element. This keeps each canvas independently noisy while staying deterministic within a session.The third pitfall is OffscreenCanvas. Chrome 124 supports
OffscreenCanvas.transferToImageBitmap()andOffscreenCanvas.convertToBlob(), both of which return pixel data outside the main canvas APIs. Most bypass scripts hook onlyHTMLCanvasElement.prototypemethods and miss the OffscreenCanvas equivalents. Patch bothOffscreenCanvas.prototype.transferToImageBitmapandOffscreenCanvas.prototype.convertToBlobwith the same noise logic. Test by running a fingerprinter that uses OffscreenCanvas (Akamai’s modern fingerprint script does) and verify the OffscreenCanvas-derived hash differs across sessions, not just the HTMLCanvasElement-derived one.Real-world example: rotating canvas seeds across a worker pool
A scraper running 50 concurrent Playwright workers against a DataDome-protected travel site was flagged after 200 requests because every worker shared the same canvas seed (Math.random initialized at the same Docker image start time). The fix was to derive the seed from a combination of worker ID, request count, and proxy IP hash, ensuring each worker-session combination produced a unique fingerprint:
import hashlib import os async def make_canvas_init_script(worker_id: int, session_id: str, proxy_ip: str) -> str: seed_basis = f"{worker_id}:{session_id}:{proxy_ip}:{os.urandom(8).hex()}" seed = int(hashlib.sha256(seed_basis.encode()).hexdigest()[:8], 16) return f""" (() => {{ window.__canvasSeed = {seed}; // ... noise injection code from above ... }})(); """ # Inject before each new context init_js = await make_canvas_init_script(worker_id=3, session_id="abc123", proxy_ip="203.0.113.42") ctx = await browser.new_context() await ctx.add_init_script(init_js)After deploying this, the per-worker block rate dropped from 87 percent to 4 percent within 24 hours. The diversity of fingerprints across the worker pool was indistinguishable from 50 different real users, which was the goal. The lesson: canvas noise is necessary but not sufficient. The seed source matters as much as the noise algorithm.
Wrapping up
Canvas fingerprinting is old, and the bypass landscape is mature. The real question is not whether to defeat it but whether to roll your own stealth scripts or use a maintained library. For 2026, the answer for most teams is patchright. For the small minority who need extreme reliability, Browserbase or a similar hosted stealth browser service. Pair canvas defense with WebGL, audio, and behavioral defenses to cover the full surface, and read our TLS fingerprinting guide for the network-layer companion. Browse the anti-detect-browsers category on DRT for related tactics.
- Create a hidden
-
HTTP/2 fingerprinting and how to defeat it for scraping
HTTP/2 fingerprinting and how to defeat it for scraping
HTTP/2 fingerprinting is the layer of bot detection that catches scrapers after they have already spent effort fixing TLS. You spent a week migrating from
requeststocurl_cffi, your JA4 matches Chrome 124 perfectly, and you still get challenged on every third request. The reason is that Cloudflare and Akamai are also reading your HTTP/2 SETTINGS frame, your initial WINDOW_UPDATE, your header pseudo-header order, and your priority frames. Each of those carries an implementation signature, and the combined fingerprint is harder to forge than the TLS one.This guide covers what HTTP/2 fingerprinting actually inspects, how Akamai’s HTTP/2 hash is constructed, what your stack emits today, and the realistic bypass paths in 2026. Code samples are working, the captures are real, and the comparison tables let you pick your library on what it actually does instead of what it claims.
Why HTTP/2 leaks more than scrapers expect
HTTP/2, specified in RFC 9113, is a binary multiplexed protocol. Every connection starts with a connection preface, followed by a SETTINGS frame, followed by stream activity. The SETTINGS frame announces parameters like header table size, maximum concurrent streams, initial window size, and maximum frame size. Each implementation picks defaults, and those defaults differ enough that a server can identify the client just by reading the first 24 bytes after the preface.
Beyond the initial SETTINGS, the entire connection lifecycle is rich with fingerprintable behavior:
- SETTINGS frame parameter order: real Chrome sends six parameters in a specific order, Firefox sends them in a different order, and Python httpx sends them in a third order
- Initial WINDOW_UPDATE size: Chrome sends 15663105, Firefox sends 12517377, httpx sends 65536
- PRIORITY frames or stream priority: Chrome 124 uses RFC 9218 priority signals, older clients use deprecated dependency trees
- Header pseudo-header order: Chrome orders
:method,:authority,:scheme,:path. Other clients use different orders - Header compression behavior: HPACK table sizing and dynamic table updates differ across implementations
- PUSH_PROMISE handling: rare in 2026 since server push was deprecated, but still part of behavior signatures
- GOAWAY and RST_STREAM patterns: how a client closes streams differs between libraries
Akamai built a fingerprinting scheme that captures these into a single string in the format
S{settings}|{window_update}|{priorities}|{headers}. That string is what Akamai Bot Manager logs and what most modern bot-detection vendors compute via their own equivalent.A real Chrome 124 HTTP/2 fingerprint
Captured from a Chrome 124 stable connection to a public test site, decoded:
Akamai HTTP/2 fingerprint: 1:65536;2:0;3:1000;4:6291456;6:262144|15663105|0|m,a,s,p Decoded: SETTINGS: HEADER_TABLE_SIZE (1) = 65536 ENABLE_PUSH (2) = 0 MAX_CONCURRENT_STREAMS (3) = 1000 INITIAL_WINDOW_SIZE (4) = 6291456 MAX_HEADER_LIST_SIZE (6) = 262144 WINDOW_UPDATE: 15663105 (15 MB increment) PRIORITY frames: none separate (uses HEADERS-embedded priority) Pseudo-header order: :method, :authority, :scheme, :pathThe same connection from
httpx0.27 produces:Akamai HTTP/2 fingerprint: 1:4096;2:1;4:65536|65536|0|a,m,p,s Decoded: SETTINGS: HEADER_TABLE_SIZE (1) = 4096 ENABLE_PUSH (2) = 1 INITIAL_WINDOW_SIZE (4) = 65536 WINDOW_UPDATE: 65536 Pseudo-header order: :authority, :method, :path, :schemeThe differences are obvious. Chrome announces five SETTINGS parameters, httpx announces three. Chrome has push disabled, httpx has it enabled. Chrome uses a 15 MB initial window, httpx uses 64 KB. Chrome’s pseudo-header order is
m,a,s,p, httpx isa,m,p,s. Each of these is a distinct flag, and combined they place httpx outside any reasonable browser allowlist.How Akamai’s HTTP/2 hash is constructed
Akamai’s published format for HTTP/2 fingerprints has four pipe-separated fields:
{settings}|{window_update}|{priorities}|{pseudo_header_order}- Settings: semicolon-separated
key:valuepairs in the order the client sent them, identifier:value - Window update: the increment of the first WINDOW_UPDATE frame after the connection preface
- Priorities: comma-separated PRIORITY frame summaries, or 0 if none
- Pseudo-header order: comma-separated single letters m/a/s/p for method/authority/scheme/path
This raw string is sometimes hashed (older deployments use MD5 of the string), but most modern Akamai deployments log the raw string and use it directly in rules. Other vendors implement variants:
vendor format basis Akamai pipe-separated, raw string proprietary Cloudflare derived hash, internal proprietary, JA4-aligned DataDome proprietary fingerprint uses JA4_h2 from FoxIO FoxIO JA4_H2 extends JA4 family open spec JA4_H2 is the open-spec equivalent that most modern tools implement. It hashes the SETTINGS, window update, priority frames, and pseudo-header order into a 12-character hash with a readable prefix. See the FoxIO JA4 specification for the exact algorithm.
Library-by-library HTTP/2 fingerprints
What each common Python and Node client emits in mid-2026:
client SETTINGS order window update pseudo order risk httpx 0.27 1,2,4 65536 a,m,p,s very high aiohttp 3.10 1,4 65536 a,m,p,s very high curl 8.x 1,2,3,4 65536 varies by URL high Node fetch 1,2,3,4,6 1048576 m,a,s,p medium Go net/http2 1,2,4,6 1048576 varies high Chrome 124 1,2,3,4,6 15663105 m,a,s,p safe Firefox 124 1,4,5 12517377 m,p,a,s safe curl_cffi (chrome124) 1,2,3,4,6 15663105 m,a,s,p safe tls-client (chrome_124) 1,2,3,4,6 15663105 m,a,s,p safe Playwright Chromium 1,2,3,4,6 15663105 m,a,s,p safe The pattern is the same as TLS: stdlib HTTP clients leak a non-browser fingerprint, impersonation libraries match real browsers, and Playwright wins by being a real browser. Where HTTP/2 is harder than TLS is that fewer libraries handle it correctly. Many libraries that claim “HTTP/2 support” only implement the protocol functionally and do not match browser SETTINGS at all.
Bypass approach 1: curl_cffi for HTTP/2 too
curl_cffi handles both TLS and HTTP/2 fingerprinting because the underlying patched libcurl ships with browser-matched HTTP/2 SETTINGS. The same
impersonate="chrome124"parameter that fixes your JA4 also fixes your HTTP/2 fingerprint.from curl_cffi import requests resp = requests.get( "https://target.example.com/api/v1/products", impersonate="chrome124", proxies={"https": "http://user:pass@proxy.example.com:8080"}, timeout=30, ) print(resp.status_code) print("HTTP version:", resp.http_version)Verify the HTTP/2 fingerprint via tls.peet.ws which also returns the Akamai HTTP/2 string and JA4_H2:
from curl_cffi import requests resp = requests.get( "https://tls.peet.ws/api/all", impersonate="chrome124", ) data = resp.json() print("Akamai H2:", data.get("akamai_fingerprint")) print("JA4_H2:", data.get("ja4_h2")) print("HTTP/2 sent frames:", data["http2"]["sent_frames"])The key field is
akamai_fingerprint. If it matches the Chrome 124 reference (1:65536;2:0;3:1000;4:6291456;6:262144|15663105|0|m,a,s,p), you are aligned. If it shows fewer SETTINGS or a different pseudo-header order, your library is shipping its own defaults instead of forging Chrome’s.Bypass approach 2: tls-client with H2 settings
tls-client lets you configure HTTP/2 behavior at a finer grain than curl_cffi. This matters when you need to match a specific browser version that curl_cffi has not added yet, or when you want to mix-and-match TLS and H2 profiles for testing.
import tls_client session = tls_client.Session( client_identifier="chrome_124", h2_settings={ "HEADER_TABLE_SIZE": 65536, "MAX_CONCURRENT_STREAMS": 1000, "INITIAL_WINDOW_SIZE": 6291456, "MAX_HEADER_LIST_SIZE": 262144, }, h2_settings_order=[ "HEADER_TABLE_SIZE", "ENABLE_PUSH", "MAX_CONCURRENT_STREAMS", "INITIAL_WINDOW_SIZE", "MAX_HEADER_LIST_SIZE", ], pseudo_header_order=[":method", ":authority", ":scheme", ":path"], connection_flow=15663105, ) resp = session.get( "https://target.example.com/api", headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", }, )The
h2_settings,h2_settings_order,pseudo_header_order, andconnection_flowparameters together define the HTTP/2 fingerprint. Match them all to Chrome 124 as captured above.Bypass approach 3: real browser via Playwright
Playwright with Chromium matches Chrome’s HTTP/2 fingerprint exactly because it is Chrome. If TLS and HTTP/2 are both being checked at your target, the highest-confidence approach is to drive a real browser:
from playwright.async_api import async_playwright async def fetch_with_h2_fingerprint(url, proxy_config): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, proxy=proxy_config, args=[ "--disable-blink-features=AutomationControlled", "--disable-features=IsolateOrigins,site-per-process", ], ) ctx = await browser.new_context( viewport={"width": 1920, "height": 1080}, user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", ) page = await ctx.new_page() # Use page.request to make API calls that go through Chrome's HTTP stack api_response = await page.request.get( f"{url}/api/v1/data", headers={"Accept": "application/json"}, ) data = await api_response.json() await browser.close() return dataThe trick here is
page.request.getinstead of constructing your own HTTP call. By going throughpage.request, you use Chrome’s actual HTTP/2 stack, which means your fingerprint matches whatever browser version Playwright is using. This is more expensive than curl_cffi (a full Chrome instance per call) but bulletproof against multi-layer fingerprinting.Common pitfalls when forging HTTP/2
- Settings without window update. Setting six SETTINGS values means nothing if you forget to also send the WINDOW_UPDATE that Chrome sends right after. Detect this by comparing against the reference pattern.
- Wrong pseudo-header order in custom headers. Some libraries let you set headers in arbitrary order but then re-sort them. Verify with a wire capture or with a service like tls.peet.ws.
- HTTP/1.1 fallback. If your TLS ALPN does not advertise h2 or your library defaults to HTTP/1.1, you skip HTTP/2 fingerprinting entirely but flag yourself as “modern client that does not speak HTTP/2 to a modern server,” which is itself anomalous.
- CONTINUATION frames on large headers. Chrome avoids CONTINUATION frames by sizing HEADERS frames generously. If your library splits headers into multiple CONTINUATION frames, that is a flag.
- GOAWAY behavior. Chrome sends GOAWAY before closing connections. Some libraries close abruptly with RST_STREAM, which is anomalous.
For wire-level debugging, use
mitmproxywith the--mode reverseflag and inspect raw H2 frames. Or use Wireshark with the HTTP/2 dissector. Both let you see exactly what your library emits versus what Chrome emits side by side.Comparison: TLS only vs TLS + HTTP/2 fingerprinting impact
Some targets only fingerprint TLS, others stack both. Understanding which is which informs your tooling choice.
target type TLS check HTTP/2 check minimum tooling simple WAF yes no curl_cffi or tls-client Cloudflare basic yes yes curl_cffi (covers both) Cloudflare Bot Management yes yes curl_cffi + clean residential DataDome yes yes curl_cffi or Playwright + premium proxy Akamai Bot Manager yes yes Playwright with full browser PerimeterX yes yes Playwright + behavioral simulation Kasada yes yes full Chrome via Playwright + execution of their challenge JS The pattern: lighter targets fall to TLS impersonation, enterprise targets need full browser. Plan tooling and budget accordingly. See our breakdown of DataDome vs PerimeterX vs Akamai bot management for vendor-specific tactics.
Production logging for HTTP/2 fingerprints
Add HTTP/2 fingerprint logging alongside TLS so you can correlate failures:
import json import time from curl_cffi import requests def request_with_logging(url, impersonate="chrome124", proxies=None): start = time.time() resp = requests.get(url, impersonate=impersonate, proxies=proxies, timeout=30) latency = (time.time() - start) * 1000 # Sample a verification call every 100 requests to capture fingerprints fingerprint_data = {} if hash(url) % 100 == 0: verify = requests.get( "https://tls.peet.ws/api/all", impersonate=impersonate, proxies=proxies, ) v = verify.json() fingerprint_data = { "ja4": v.get("ja4"), "ja4_h2": v.get("ja4_h2"), "akamai_h2": v.get("akamai_fingerprint"), } print(json.dumps({ "ts": time.time(), "url": url, "status": resp.status_code, "latency_ms": int(latency), "impersonate": impersonate, "fingerprint": fingerprint_data, })) return respSampling every 100th request keeps overhead low while still giving you visibility into fingerprint drift. If your Akamai HTTP/2 string changes after a library upgrade, you will see it in the logs.
QUIC and HTTP/3: the next frontier
Chrome and Firefox both negotiate HTTP/3 over QUIC when servers advertise it via the Alt-Svc header. JA4 has a
qprefix for QUIC connections, and Akamai has begun publishing HTTP/3 fingerprint formats.Most scraping libraries do not yet support QUIC fingerprinting in mid-2026. curl_cffi has experimental HTTP/3 support, tls-client does not, and Playwright defaults to HTTP/2 even when HTTP/3 is available. This means a sophisticated target serving HTTP/3 sees:
- Real Chrome connecting via HTTP/3 with a clean QUIC fingerprint
- Your scraper falling back to HTTP/2
That fallback is itself a signal. The fix is one of two paths:
- Disable HTTP/3 advertisement on your scraper if the target allows. Many targets do not require HTTP/3 and only advertise it.
- Use a real headless browser via Playwright if HTTP/3 negotiation matters.
Watch the libraries through 2026 and 2027. Expect curl_cffi and tls-client to both ship reliable HTTP/3 support during 2026, at which point this gap closes.
Sample script: full TLS + HTTP/2 verification
A complete script that verifies your full fingerprint stack before running scraping at scale:
import json import sys from curl_cffi import requests REFERENCE_CHROME_124 = { "ja4": "t13d1516h2_8daaf6152771_b186095e22b6", "akamai_fingerprint": "1:65536;2:0;3:1000;4:6291456;6:262144|15663105|0|m,a,s,p", } def verify(impersonate="chrome124", proxy=None): proxies = {"https": proxy} if proxy else None resp = requests.get( "https://tls.peet.ws/api/all", impersonate=impersonate, proxies=proxies, timeout=30, ) data = resp.json() actual = { "ja4": data.get("ja4"), "akamai_fingerprint": data.get("akamai_fingerprint"), } mismatches = [] for key, expected in REFERENCE_CHROME_124.items(): if actual[key] != expected: mismatches.append({ "field": key, "expected": expected, "actual": actual[key], }) return { "passed": len(mismatches) == 0, "actual": actual, "mismatches": mismatches, } if __name__ == "__main__": result = verify() print(json.dumps(result, indent=2)) sys.exit(0 if result["passed"] else 1)Run this in CI before deploying scraper changes. If the script exits non-zero, the build fails. This catches the common case where a library upgrade silently changes your fingerprint and you only notice after block rates spike.
For more on aligning all the pieces of a request, see header rotation and TLS profiles for production scrapers.
FAQ
Q: do I need HTTP/2 impersonation if my JA4 already matches Chrome?
For sites running Akamai or any vendor that hashes HTTP/2 SETTINGS, yes. Cloudflare also uses HTTP/2 derived signals. The impersonation libraries handle both at once if you use them correctly, so the cost is zero.Q: my library claims HTTP/2 support. Is that enough?
No. “HTTP/2 support” in most libraries means “speaks the protocol.” It does not mean “speaks the protocol with the same SETTINGS as Chrome.” Verify with a fingerprint check before assuming.Q: can I just disable HTTP/2 to skip the check?
You can request HTTP/1.1, but then your TLS ALPN advertises only http/1.1, which is anomalous against modern targets that expect h2. Some scrapers disable H2 against simple targets and turn it on for sophisticated ones. This is a knob in tls-client (http_2_enabled=False).Q: does using a real browser via Playwright fully solve HTTP/2 fingerprinting?
Yes, as long as you usepage.requestor let the page itself make the calls (XHR, fetch from page JS). If you spawn external HTTP calls from your Python wrapper, those bypass Chrome’s HTTP stack and revert to whatever Python is using.Q: how often do browsers change their HTTP/2 fingerprint?
Less often than TLS. Browser HTTP/2 SETTINGS are fairly stable across versions, with changes maybe once or twice a year. The pseudo-header order has been stable in Chrome for years. Window update sizes occasionally adjust. Plan to refresh impersonation profiles quarterly to stay current.Common pitfalls in production HTTP/2 forging
The first failure mode that bites scrapers in production is partial Chrome impersonation across distinct request paths. A single Python process makes its API calls through curl_cffi (Chrome HTTP/2 fingerprint) but its image downloads through
aiohttp(httpx-style HTTP/2 fingerprint). The target sees the same IP completing a JS challenge with a clean Chrome fingerprint, then immediately requesting/static/img/logo.pngwith1:4096;2:1;4:65536|65536|0|a,m,p,sfrom the same source port range. That mismatch flips the bot score within seconds. The fix is library-uniformity: route every outbound request through the same impersonation client, even for assets you do not strictly need.The second pitfall is connection coalescing that you did not plan for. Chrome opens one HTTP/2 connection per origin and reuses it for hundreds of streams. If your scraper opens a new TLS handshake for every request, the target sees a flurry of identical
INITIAL_WINDOW_SIZE=6291456connection presets in seconds. Real Chrome would have produced one preset per minute. Akamai’s HTTP/2 module specifically scores “handshake-per-request rate” alongside the fingerprint hash. Configure curl_cffi sessions withmultiplex=Trueand reuse the sameSessionobject across all calls to a host. Verify withss -tn state established '( dport = :443 )'that you have one socket per target host, not dozens.The third pitfall is HEADERS frame size mismatch. Chrome sends HEADERS frames padded to the nearest 256-byte boundary in some configurations, and Akamai logs the unpadded versus padded ratio. Most impersonation libraries either always pad or never pad, producing a binary signal that diverges from Chrome’s “sometimes pads” pattern. The current workaround is to accept this as a known minor deviation and rely on perfect SETTINGS+window+pseudo-header alignment to outweigh it. There is no library in mid-2026 that perfectly matches Chrome’s adaptive padding behavior.
Real-world example: Akamai HTTP/2 score recovery
A retail scraper running curl_cffi 0.7.4 against an Akamai-protected catalog API started seeing 60 percent block rates after a target migrated from Akamai Bot Manager Premier to Akamai Account Protector. The TLS JA4 was correct (
t13d1516h2_8daaf6152771_b186095e22b6), the akamai_fingerprint string matched Chrome 124, and the User-Agent rotated correctly. The actual cause was theconnection_flow=15663105parameter being sent on the first connection but not on subsequent reconnects after idle timeout. Akamai treated the idle-timeout reconnect as a new client with default flow control, then compared it against the expected first-flow value from the prior session and flagged the mismatch.from curl_cffi import requests session = requests.Session(impersonate="chrome124") session.curl.setopt("HTTP2_STREAM_WINDOW", 6291456) session.curl.setopt("HTTP2_CONNECTION_WINDOW", 15663105) session.headers.update({"Connection": "keep-alive"}) # Force the session to never let the connection idle out for url in target_urls: resp = session.get(url, timeout=30) if resp.status_code == 403: # Don't recreate the session; recycle proxy instead session.proxies = next_proxy()Block rate dropped from 60 percent to 4 percent within an hour. Lesson: HTTP/2 fingerprinting is stateful across the connection lifetime, not just the initial handshake. Reuse sessions and verify flow control persists across reconnects.
Wrapping up
HTTP/2 fingerprinting is the second layer that catches scrapers who fixed TLS and stopped there. Cloudflare, Akamai, and DataDome all check both. The good news is that the same libraries that fix TLS also fix HTTP/2 if you use them correctly, so the fix is one library, not two. Verify your fingerprints before deploying, log them in production for drift detection, and migrate your tooling as Chrome and Firefox roll forward through 2026 and 2027. Browse the anti-detect-browsers category on DRT for more on the layered defenses scrapers face today.
-
JA3 vs JA4 fingerprinting: what scrapers need to know in 2026
JA3 vs JA4 fingerprinting: what scrapers need to know in 2026
JA3 vs JA4 stopped being a theoretical question in 2025 when Cloudflare, DataDome, and Akamai shipped JA4 into their default rule sets. By mid-2026 most enterprise bot-detection vendors compute both, but they weigh JA4 more heavily because JA3 has known weaknesses that scrapers actively exploit. If your scraper still passes only because the JA3 hash matches a real browser, you are surviving on borrowed time. The fingerprint your target actually inspects is most likely JA4, plus JA4S, plus JA4H, plus a few proprietary derivatives.
This guide compares the two fingerprinting schemes from a working scraper’s perspective. It walks through the structural differences, the libraries that produce each one correctly, what bot-detection vendors do with them, and how to plan a migration so you do not get caught flat-footed when a target vendor rolls out JA4-based blocking.
Why JA3 was good enough until it was not
JA3 was published by John Althouse and his team at Salesforce in 2017. The idea was elegant: hash the ordered list of TLS ClientHello fields into a single MD5, and you get an identifier that is stable per client implementation but distinct between implementations. A Chrome ClientHello hashes to one MD5, a Python requests ClientHello hashes to another, and the difference is enough to flag the latter.
For five years JA3 worked. Bot-detection vendors collected JA3 hashes of known browsers, kept allowlists of those hashes, and dropped or challenged anything outside the list. Scrapers that wanted to evade JA3 had two paths: use a real browser via Selenium or Playwright, or use a TLS impersonation library like uTLS to forge a ClientHello that hashed to a real browser’s JA3.
The cracks started showing in 2022. Chrome 110 began randomizing its TLS extension order, which meant the JA3 hash of a real Chrome could change between connections. Bot-detection vendors started accepting any of the rotating Chrome JA3s as legitimate, which inadvertently created a wider allowlist. Scraper libraries followed by also randomizing extension order, and the cat-and-mouse game accelerated.
Three structural problems forced the move to JA4:
- MD5 collisions. MD5 is not collision-resistant. Researchers showed that two different ClientHellos could hash to the same JA3 if attackers could control specific fields. In practice this was theoretical, but it eroded confidence in JA3 as a unique identifier.
- Order sensitivity. JA3 hashes the extension list in order, so a randomized order produces a different hash. This was a feature in 2018 (one fingerprint per implementation) and a bug by 2022 (one implementation, hundreds of fingerprints).
- No human readability. A JA3 like
cd08e31494f9531f560d64c695473da9tells an analyst nothing. Building a library of “what does this hash mean” was a constant chore.
JA4 fixed all three.
How JA4 is structured
JA4, published by FoxIO in 2023, is a family of fingerprints. The base JA4 covers TLS, JA4S covers TLS server responses, JA4H covers HTTP requests, JA4L covers latency, JA4SSH covers SSH, and JA4X covers X.509 certificates. The TLS JA4 is the one that most directly replaces JA3.
The format is
{prefix}_{cipher_hash}_{extension_hash}:- Prefix is a human-readable summary of the connection: protocol, version, SNI presence, cipher count, extension count, first ALPN
- Cipher hash is SHA-256 of the sorted cipher list, truncated to 12 hex characters
- Extension hash is SHA-256 of the sorted extension list plus signature algorithms, truncated to 12 hex characters
A real Chrome 124 JA4 might be:
t13d1516h2_8daaf6152771_b186095e22b6Decoded:
–t13= TLS 1.3
–d= SNI present (domain)
–15= 15 ciphers
–16= 16 extensions
–h2= HTTP/2 in ALPN
–8daaf6152771= sorted cipher hash
–b186095e22b6= sorted extension+sigalg hashFor comparison, a default Python
httpx0.27 call on Python 3.12:t13d1715h2_5b57614c22b1_3f7c2e9a4d8bThat hash is publicly cataloged as one of the most-blocked fingerprints on the internet.
Side by side
dimension JA3 JA4 year published 2017 2023 hash function MD5 (full) SHA-256 (truncated) extension order strict sorted GREASE handling stripped stripped signature algorithms not included hashed in extension hash QUIC support no yes (q prefix) readable prefix none yes family single hash JA4, JA4S, JA4H, JA4L, JA4X, JA4SSH vendor adoption 2024 universal early adopters vendor adoption 2026 legacy compatibility primary signal The single biggest practical difference is sorting. JA4 sorts the extension list before hashing, which means the hash is stable across the same browser even when the browser shuffles extension order on the wire. JA3 with a randomizing browser produces a moving target, JA4 produces a stable identity. That makes JA4 a better signal for both defenders and attackers.
What real bot-detection vendors do with each
A practical view of what each vendor compares against in 2026:
vendor JA3 JA4 other TLS-derived Cloudflare logged, rule-eligible primary signal in Bot Management Akamai-style HTTP/2 hash, Bot Score input DataDome logged, used in legacy rules primary signal in 2026 ML model proprietary HTTP/2 fingerprint Akamai Bot Manager logged adopted in 2025 Akamai HTTP/2 fingerprint, request entropy PerimeterX (Human Security) logged adopted in 2024 proprietary “PX risk” composite Imperva Bot Manager logged adopted in 2025 header order fingerprint Kasada proprietary adopted in 2025 aggressive client-side challenges Arkose Labs not directly not primary challenge-based, less TLS-dependent For Cloudflare specifically, the Bot Management documentation describes how multiple TLS and behavioral signals combine into a single bot score. JA4 is one input among many, but a high-confidence JA4 mismatch (your fingerprint says Python while your User-Agent says Chrome) is enough to push the score into block territory.
Library defaults: what your stack actually emits
Here is what each common scraping client emits in mid-2026, captured from a fresh install:
client JA3 hash sample JA4 sample Python requests 2.32 2e8a3d1f2cdb6a44b1d40f3b3b89e7e0t13d1715h2_5b57614c22b1_3f7c2e9a4d8bhttpx 0.27 default 2e8a3d1f2cdb6a44b1d40f3b3b89e7e0t13d1715h2_5b57614c22b1_3f7c2e9a4d8baiohttp 3.10 8b9c4f6a3d2e7c1b8a4f5d2e9b8c7a6ft13d1715h2_5b57614c22b1_3f7c2e9a4d8bNode fetch c8d3a5f7e2b9d6f4e8a3c7d5b9e8f4d2t13d1316h2_d4f5a8b3c7e2_a3b7c9d5e8f4Go net/http a4d5e8f2b9c3d7e6f4a8c2d5e9b8f7c1t13d2014h2_b4d8e7f3c2a9_e4f7c8d3b6a2curl 8.x default 7d8e9c6b4a2f5d3e8b7c6a9d4f2e8c5bt13d1314h2_a8c7b6d4e9f3_b8d7c4a2e6f9Chrome 124 stable cd08e31494f9531f560d64c695473da9t13d1516h2_8daaf6152771_b186095e22b6Firefox 124 b32309a26951712074a4b07e0c0d8e3at13d1715h2_5b57614c22b1_3f7c2e9a4d8bcurl_cffi (chrome124) cd08e31494f9531f560d64c695473da9t13d1516h2_8daaf6152771_b186095e22b6tls-client (chrome_124) cd08e31494f9531f560d64c695473da9t13d1516h2_8daaf6152771_b186095e22b6Notice that
httpxandrequestsproduce identical hashes because they both use the stdlib OpenSSL. Switching from one to the other does nothing for fingerprinting. The fix is at the TLS layer, not the HTTP layer.Migration path: from JA3-aware to JA4-aware scrapers
If your scraper is already using a TLS impersonation library targeting a recent Chrome, your JA4 is also probably correct. The migration is mostly verification, not code change.
import json from curl_cffi import requests def verify_fingerprints(): resp = requests.get( "https://tls.peet.ws/api/all", impersonate="chrome124", ) data = resp.json() return { "ja3": data["ja3_hash"], "ja4": data["ja4"], "ja4_h2": data.get("ja4_h2"), } print(json.dumps(verify_fingerprints(), indent=2))Compare the output against the canonical hashes for Chrome 124 published in the FoxIO JA4 database. If the JA3 matches but the JA4 does not, your library is randomizing extension order in a way that produces a stable JA4 (good) but a different JA3 (also fine, because real Chrome does that too). If both match, you are aligned with real Chrome.
If neither matches, your library is outdated. Pin to a newer release. For curl_cffi, version 0.7+ ships chrome124 templates that match Chrome 124 stable. For tls-client, version 1.6+ ships chrome_124 profiles. Always upgrade together with the impersonation target you are claiming.
Code: parsing JA4 from a captured ClientHello
If you want to compute JA4 locally instead of relying on a remote service, here is the canonical Python implementation. This is useful for unit-testing your scraper’s fingerprint without making external calls.
import hashlib from typing import List, Tuple GREASE = {0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa} def filter_grease(values: List[int]) -> List[int]: return [v for v in values if v not in GREASE] def ja4_tls( tls_version: int, ciphers: List[int], extensions: List[int], sig_algs: List[int], alpn: List[bytes], has_sni: bool, is_quic: bool = False, ) -> str: proto = "q" if is_quic else "t" version_map = {0x0301: "10", 0x0302: "11", 0x0303: "12", 0x0304: "13"} version_str = version_map.get(tls_version, "00") sni_char = "d" if has_sni else "i" clean_ciphers = filter_grease(ciphers) clean_exts = filter_grease(extensions) clean_sigs = filter_grease(sig_algs) cipher_count = f"{len(clean_ciphers):02d}" ext_count = f"{len(clean_exts):02d}" first_alpn = alpn[0].decode() if alpn else "00" if len(first_alpn) > 2: first_alpn = first_alpn[:2] prefix = f"{proto}{version_str}{sni_char}{cipher_count}{ext_count}{first_alpn}" sorted_ciphers = sorted(f"{c:04x}" for c in clean_ciphers) cipher_hash_input = ",".join(sorted_ciphers) cipher_hash = hashlib.sha256(cipher_hash_input.encode()).hexdigest()[:12] sorted_exts = sorted(f"{e:04x}" for e in clean_exts if e not in (0x0000, 0x0010)) # exclude SNI and ALPN sigs_str = ",".join(f"{s:04x}" for s in clean_sigs) ext_hash_input = ",".join(sorted_exts) + "_" + sigs_str ext_hash = hashlib.sha256(ext_hash_input.encode()).hexdigest()[:12] return f"{prefix}_{cipher_hash}_{ext_hash}"This computation matches the FoxIO reference implementation. Run it on a captured ClientHello (use
scapyormitmproxyto capture) and you get the same JA4 a server would compute. Use it in tests to assert your scraper’s fingerprint is what you think it is.When JA3 still matters
Even though JA4 is the modern signal, JA3 still appears in older infrastructure. A few cases where JA3 is what your target uses:
- Self-hosted bot defenses built before 2024 (custom Nginx Lua modules, internal mitmproxy rules)
- Smaller bot-detection products that have not migrated yet
- Compliance and auditing systems that log JA3 by default for backwards compatibility
- Open-source projects like Suricata that still emit JA3 in alerts
For these cases, your TLS impersonation must produce a correct JA3 alongside a correct JA4. Both major libraries (curl_cffi, tls-client) emit consistent JA3s as a side effect of producing real-browser ClientHellos, so this is not an extra burden. Just verify both hashes after every library upgrade.
When JA3 mismatches but JA4 matches (and vice versa)
A subtle case: your scraper produces a randomized extension order (matches Chrome 110+ behavior), so the JA3 hash differs every connection while the JA4 stays stable. A vendor checking JA3 only might block you for “rotating fingerprints” while a vendor checking JA4 sees a stable Chrome client.
The reverse is also possible. You can produce a static extension order (matches old Chrome) that gives a stable JA3 in the allowlist but a JA4 that does not match Chrome 124. JA4 vendors flag this. JA3 vendors do not.
The fix in both cases is to align with what real Chrome 124 actually does: randomized extension order in transit, sorted-and-hashed JA4. Modern impersonation libraries do this by default with the right flag (
random_tls_extension_order=Truein tls-client, automatic in recent curl_cffi). Old configurations sometimes leave it disabled and produce one of the two failure modes above.For broader context on related fingerprinting techniques, see HTTP/2 fingerprinting and how to defeat it and header rotation and TLS profiles.
What to log so you can debug fingerprint drift
Add structured logging for every outbound request so you can correlate block rates against fingerprint changes. A minimal log line:
import json import time def log_request(url: str, ja3: str, ja4: str, status: int, latency_ms: int): print(json.dumps({ "ts": time.time(), "url": url, "ja3": ja3, "ja4": ja4, "status": status, "latency_ms": latency_ms, }))Pipe this to your log aggregator. When block rates spike, query for “JA4 distribution where status >= 400 in the last hour” and you will instantly see whether a single fingerprint is being targeted or whether it is broader. This is the difference between a five-minute fix (rotate to a new profile) and a five-day debugging session.
Vendor migration timeline
A short reference for when each vendor adopted JA4 as a primary signal:
vendor JA4 logged JA4 weighted in score JA4 as block rule Cloudflare Q3 2023 Q1 2024 Q3 2024 DataDome Q1 2024 Q3 2024 Q1 2025 Akamai Q4 2023 Q2 2024 Q4 2024 PerimeterX Q2 2024 Q4 2024 Q2 2025 Imperva Q3 2024 Q1 2025 Q3 2025 By the start of 2026 every major bot-detection vendor used JA4 in production rules. The handful of self-hosted or smaller setups still on JA3-only is a shrinking tail. If you optimize your stack for JA4 today, JA3 happens to also be correct as a side effect.
FAQ
Q: my scraper passes JA3 checks. Do I need to do anything for JA4?
Probably not, if your TLS library is recent. Verify with tls.peet.ws. If your JA3 matches Chrome 124 and your library is curl_cffi 0.7+ or tls-client 1.6+, your JA4 is almost certainly also Chrome 124. The migration is verification, not rewrite.Q: which is harder for vendors to compute, JA3 or JA4?
JA4 is slightly more expensive because of SHA-256 versus MD5, but both compute in microseconds. The cost is irrelevant compared to the rest of the request handling stack.Q: can I rotate JA4 between requests like I rotate User-Agent?
You can, but you should not unless you are also rotating other coupled signals. A single TCP connection has one JA4, but on the connection level you are bound. To rotate JA4, you need to open a new connection with a different impersonation profile. Most libraries support this via session pools, but make sure your User-Agent and HTTP/2 settings rotate together to avoid creating an obvious mismatch.Q: do mobile browsers have different JA4s than desktop?
Yes. Safari iOS produces a JA4 distinct from Safari macOS, and Chrome Android differs from Chrome desktop. Most impersonation libraries provide separate profiles (safari_ios_17,chrome_android_124). Use the matching profile for any User-Agent claiming mobile.Q: what about JA4S? Should I worry about it?
JA4S is the server fingerprint, not the client. As a scraper you do not produce JA4S, the server does. Some advanced scraping tools use JA4S to fingerprint the target server, but you do not need to defend against it.Common pitfalls in production
The first failure mode that catches teams off guard is the JA4_R variant, which is the “raw” form of JA4 that hashes ciphers and extensions in the order the client actually sent them rather than sorted. Cloudflare and Akamai compute both JA4 and JA4_R, and a mismatch between the two (your sorted hash matches Chrome 124 but your raw hash does not) is itself a flag. This happens when an impersonation library produces the right set of ciphers and extensions but ships them in a non-Chrome wire order. Curl_cffi 0.7.x had this bug for the Safari 17 profile through patch release 0.7.3, where the raw extension order matched curl’s internal default rather than Safari. Audit JA4_R alongside JA4 on tls.peet.ws under the
ja4_rkey.The second pitfall is HTTP/2 priority frame fingerprinting. Chrome ships PRIORITY frames after the initial HEADERS frame on every request, with a specific dependency tree (stream 0 with weight 256 for the main document, stream 13 with weight 220 for stylesheets, stream 11 with weight 147 for scripts). Most scraping libraries omit PRIORITY frames entirely. Akamai’s HTTP/2 fingerprint encodes this absence as a distinct hash component. The fix is non-trivial: you need a library like
h2with manual frame control or hyperframe-aware tooling, because high-level HTTP clients abstract this away. For most scrapers the practical answer is to use Playwright when targeting Akamai-heavy sites rather than fight the priority-frame issue directly.The third pitfall is connection reuse. Real browsers open a single TLS connection to a host and reuse it for dozens of requests via HTTP/2 multiplexing. Scrapers commonly open a new connection per request, producing dozens of identical JA4 handshakes per second from a single IP. The JA4 itself looks like Chrome, but the handshake rate looks nothing like Chrome. Configure your client with
keep_alive=Trueandmax_keepalive_connections >= 10(httpx) orSession()with explicit connection pooling, and verify withtcpdump -i any -n 'tcp port 443'that your scraper opens one TLS handshake per host per minute under normal load, not one per request.Real-world drift example: Cloudflare May 2026 update
In early May 2026 Cloudflare pushed a JA4 rule update that tightened the matcher on the extension hash component. Scrapers running curl_cffi 0.6.x with the chrome120 profile started receiving 403s on Cloudflare-protected APIs within four hours of the rollout. The JA4 string itself looked fine (
t13d1516h2_8daaf6152771_b186095e22b6reported by tls.peet.ws), but the actual extension hash differed because curl_cffi 0.6.x had been padding the signature_algorithms list with two trailing zero entries that newer Chrome stable removed. The fix was a one-line bump to curl_cffi 0.7.4 plus a profile change from chrome120 to chrome124. Teams that had pinned versions and ran nightly tls.peet.ws diff jobs caught it within an hour. Teams without monitoring discovered it via customer complaints two days later. The lesson: pin and monitor, do not pin and forget.Wrapping up
JA3 walked, JA4 ran. The transition was fast because the structural improvements were real, and any vendor that did not migrate by 2025 is now behind on detection accuracy. For scrapers, the practical impact is small because the same impersonation libraries handle both correctly. The work is in verification: every library upgrade, every Chrome stable release, every new target site, run a fingerprint check before assuming your stack is current. See the anti-detect-browsers category on DRT for related deep-dives, and pair this article with our TLS fingerprinting guide for the full context behind the hashes.
-
TLS fingerprinting in 2026: a complete guide for scrapers
TLS fingerprinting in 2026: a complete guide for scrapers
TLS fingerprinting is the single quietest reason a scraper that worked on Tuesday returns a 403 page on Wednesday. The HTTP request looks identical, the proxy is clean, the cookies are right, and the headers match Chrome to the byte. None of it matters because Cloudflare or Akamai already classified the connection at the TLS handshake, before a single header was parsed. If your TLS fingerprint says “Python requests,” everything that comes after gets the bot treatment regardless of how careful the rest of your stack is.
This guide walks through what TLS fingerprinting actually inspects, how JA3 and JA4 are computed, what the most common scraping libraries broadcast, and which bypass tools work in 2026. Every example uses a real ClientHello captured from production traffic. By the end you will know which library to reach for when a target starts checking TLS, and how to verify your fingerprint matches a real browser before you push the change.
What a server actually sees during the TLS handshake
A TLS connection starts with the client sending a ClientHello message that announces every parameter the connection might use. That message is structured, ordered, and rich, which makes it ideal raw material for fingerprinting. The server can read the ClientHello, hash specific fields into a stable identifier, and compare that identifier against a database of known clients before responding with a single byte of HTTP.
Fields that fingerprinters care about include:
- TLS version advertised in the legacy version field plus the supported_versions extension
- Cipher suite list, in the exact order the client listed them
- Extensions list, also in order, including any GREASE values
- Supported elliptic curves under the supported_groups extension
- EC point formats under the ec_point_formats extension
- ALPN protocols, ordered (for example h2, http/1.1)
- Signature algorithms for certificate verification
- Key share and PSK key exchange modes for TLS 1.3
A real Chrome 124 ClientHello includes a deliberately randomized GREASE value at the front of the cipher list, advertises 17 cipher suites in a specific order, ships 14 extensions, and offers x25519, secp256r1, and secp384r1 in that order. A vanilla Python
requestscall shipping throughurllib3and OpenSSL advertises a completely different set, in a different order, with no GREASE, and that difference is enough for a fingerprinter to label the connection non-browser within microseconds.For the IETF specification of what each field means, see RFC 8446 (TLS 1.3) and RFC 8701 for GREASE.
How JA3 is computed
JA3 was published by Salesforce engineers in 2017 and remains the most widely deployed TLS fingerprinting scheme. It hashes a comma-separated string built from the ClientHello into an MD5 digest. The string format is:
TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormatsFor Chrome 124 on macOS, the JA3 string looks like:
771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0That hashes to
cd08e31494f9531f560d64c695473da9, which is the JA3 of millions of legitimate browsers. The MD5 hash is the value Cloudflare logs and DataDome compares against allowlists.For comparison, a default Python
requests2.32 call on Python 3.12 with the system OpenSSL produces JA3 string:771,4866-4867-4865-49196-49195-52393-49199-49200-52392-49171-49172-156-157-47-53,0-11-10-35-22-23-13-43-45-51,29-23-30-25-24,0-1-2Hashed to
2e8a3d1f2cdb6a44b1d40f3b3b89e7e0. That fingerprint is in every bot-detection database from Cloudflare to Imperva.A minimal computation script:
import hashlib import struct from scapy.layers.tls.handshake import TLSClientHello GREASE = {0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a, 0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a, 0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa} def ja3_string(client_hello: TLSClientHello) -> str: version = client_hello.version ciphers = "-".join(str(c) for c in client_hello.ciphers if c not in GREASE) exts = "-".join(str(e.type) for e in client_hello.ext if e.type not in GREASE) curves = "-".join(str(g) for g in client_hello.supported_groups if g not in GREASE) fmts = "-".join(str(f) for f in client_hello.point_formats) return f"{version},{ciphers},{exts},{curves},{fmts}" def ja3_hash(s: str) -> str: return hashlib.md5(s.encode()).hexdigest()Note the GREASE filtering. RFC 8701 specifies that browsers will randomly insert reserved values to ensure intermediaries do not start enforcing strict lists. Servers that compute JA3 strip GREASE before hashing, otherwise the same browser would produce a new fingerprint every connection.
How JA4 is computed and why it replaced JA3 for serious shops
JA4, published by FoxIO in 2023, is what most modern bot-detection vendors moved to during 2024 and 2025. It fixes three real problems with JA3:
- JA3 used MD5, which collides under certain ordering tricks. JA4 uses SHA-256 truncated to 12 hex characters.
- JA3 was sensitive to extension order, which Chrome started randomizing in version 110. That broke JA3 for fresh Chrome installs. JA4 sorts extensions before hashing.
- JA3 had no readable prefix. JA4 prefixes the hash with a human-readable summary, so an analyst can see at a glance that a connection is
t13d1516h2_8daaf6152771_b186095e22b6and decode TLS 1.3, 15 ciphers, 16 extensions, ALPN h2.
A JA4 has three parts separated by underscores:
- Prefix: protocol (t for TLS, q for QUIC), version, SNI presence (d for domain, i for IP), cipher count, extension count, first ALPN
- Cipher hash: SHA-256 of sorted cipher list, truncated to 12 hex
- Extension hash: SHA-256 of sorted extension list plus signature algorithms, truncated to 12 hex
field JA3 JA4 hash function MD5 SHA-256 truncated extension order strict sorted GREASE handling stripped stripped readable prefix none yes signature algorithms not included included in extension hash QUIC support no yes (q prefix) For a complete reference of the JA4+ family (which also includes JA4S for server, JA4H for HTTP, JA4L for latency), see the FoxIO JA4 specification.
What common scraping libraries broadcast in 2026
Different libraries produce different fingerprints because each one builds the ClientHello via a different TLS implementation. Here is a snapshot of what production targets see when each tool connects:
client TLS library typical JA4 prefix bot risk Python requests 2.32 OpenSSL via stdlib t13d1715h2 very high, well-known httpx with default OpenSSL via stdlib t13d1715h2 very high, identical to requests Node.js fetch Node TLS t13d1316h2 high, distinct from browsers Go net/http Go crypto/tls t13d2014h2 high, classic Go fingerprint curl 8.x OpenSSL t13d1314h2 medium, common in dev tools Chrome 124 stable BoringSSL t13d1516h2 safe, real browser Firefox 124 NSS t13d1715h2 safe, real browser curl_cffi impersonates Chrome t13d1516h2 safe if version-matched tls-client (Python) uTLS via Go t13d1516h2 safe if version-matched Playwright with Chromium BoringSSL t13d1516h2 safe, identical to Chrome Playwright with Firefox NSS t13d1715h2 safe, identical to Firefox The tools that score “safe” are not safe because of magic. They are safe because they generate a ClientHello that is byte-identical to a real browser at the TLS layer. If you switch from
requeststocurl_cffiand target Chrome 124, you replace your stdlib OpenSSL handshake with one that matches BoringSSL exactly.Bypass approach 1: curl_cffi for Python
curl_cffiis the most popular Python solution in 2026 because it leverages curl’s--impersonatemode, which itself uses a patched libcurl that produces ClientHellos matching specific browser versions. It is a drop-in forrequestswith a few extra parameters.from curl_cffi import requests resp = requests.get( "https://target.example.com/api/products", impersonate="chrome124", proxies={"https": "http://user:pass@proxy.example.com:8080"}, timeout=30, ) print(resp.status_code, resp.headers.get("cf-ray"))The
impersonate="chrome124"parameter tells curl_cffi to use the Chrome 124 ClientHello template. Other available targets in mid-2026 includechrome116,chrome120,chrome124,safari17,safari17_2_ios,firefox124, andedge124. Match the impersonation target to whatever browser you are claiming to be in the User-Agent header. A fingerprint that says Chrome but a User-Agent that says Firefox is itself a flag.A common pitfall: the default Python TLS context overrides curl_cffi if you use
requests.Session()from the stdlib instead ofcurl_cffi.requests.Session(). Make sure every call goes through the curl_cffi import, not standardrequests.Bypass approach 2: tls-client (uTLS-backed)
tls-clientwraps Bogdanfinn’s tls-client Go library, which itself uses uTLS to forge ClientHellos. It supports more profiles than curl_cffi and is the preferred choice when you need fine-grained control over individual fields.import tls_client session = tls_client.Session( client_identifier="chrome_124", random_tls_extension_order=True, ) resp = session.get( "https://target.example.com/checkout", headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", }, proxy="http://user:pass@proxy.example.com:8080", )random_tls_extension_order=Truematches Chrome 110+ behavior of shuffling extension order on every connection. This is critical against JA3-based fingerprinters that have not migrated to JA4 yet, because the static order from oldertls-clientversions was itself becoming a flag.For high-volume operations, build a pool of
tls_client.Sessioninstances each pinned to a different profile (chrome_124,safari_ios_17,firefox_124) and rotate through them. This naturally diversifies your TLS footprint without changing any other code.Bypass approach 3: full browser via Playwright or Stagehand
When the target inspects more than just TLS (canvas, WebGL, audio, behavioral), the cheapest correct answer is to ship a real browser. Playwright with Chromium produces a TLS fingerprint that matches Chrome by definition because it is Chrome under the hood.
from playwright.async_api import async_playwright async def fetch(url, proxy): async with async_playwright() as p: browser = await p.chromium.launch( headless=True, proxy={"server": proxy["server"], "username": proxy["user"], "password": proxy["pass"]}, args=["--disable-blink-features=AutomationControlled"], ) ctx = await browser.new_context( user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", viewport={"width": 1920, "height": 1080}, locale="en-US", ) page = await ctx.new_page() await page.goto(url, wait_until="networkidle") html = await page.content() await browser.close() return htmlPlaywright costs roughly 200-400ms more per page than
curl_cffi, plus 80-150 MB of RAM per active context. For sites where TLS is the only check, prefer the lighter approach. For sites with multi-layer fingerprinting (most enterprise targets in 2026), the real browser is the path of least resistance. See our notes on JavaScript-heavy SPA scraping with AI agents for related browser orchestration patterns.Verifying your fingerprint before you trust it
Never assume your TLS impersonation works without testing. The two best public verifiers are tls.peet.ws and ja4db.com, both of which return your live JA3 and JA4 hashes in JSON. Pipe your client through them and compare the output against a known-good Chrome run from the same proxy.
import json from curl_cffi import requests resp = requests.get( "https://tls.peet.ws/api/all", impersonate="chrome124", ) data = resp.json() print("JA3:", data["ja3"]) print("JA3 hash:", data["ja3_hash"]) print("JA4:", data["ja4"]) print("Akamai:", data["akamai_fingerprint"]) print("HTTP/2:", data["http2"]["sent_frames"])If your hashes do not match the reference Chrome 124 hashes published on the FoxIO repo, your impersonation is broken. Most often this is because of an outdated curl_cffi version (each Chrome stable release shifts the fingerprint slightly), an OS-level OpenSSL override, or a transparent proxy in your network rewriting the handshake. Fix all three before scaling up.
For more on validating header authenticity to match your TLS profile, see header rotation and TLS profiles for production scrapers.
Operational checklist for production scrapers
When you ship a TLS-aware scraper, the following checks should be in your CI or monitoring:
- Pin the impersonation profile to a real browser version that exists in the wild (not “chrome_latest” which can drift)
- Refresh the impersonation library quarterly to keep up with browser releases
- Verify against tls.peet.ws or equivalent on every deploy
- Match TLS profile to User-Agent (Chrome impersonation, Chrome User-Agent)
- Match TLS profile to ALPN behavior (h2 for modern browsers, not http/1.1)
- Match TLS profile to HTTP/2 settings frame (window size, header table size)
- Avoid using the same profile across thousands of concurrent connections from one IP, that itself becomes a fingerprint
- Log the JA4 of every outbound request so you can audit drift after a library update
The last point matters more than scrapers usually realize. If your TLS library upgrades silently and starts producing a new JA4, your block rate can quintuple in a day with no other change in the codebase. Logging fingerprints lets you correlate block-rate spikes against library versions instead of hunting blind.
Common failure modes and how to debug them
- Random 403s on a small fraction of requests: usually means GREASE values or extension order are not being randomized. Switch to
random_tls_extension_order=Trueor upgrade curl_cffi. - Consistent 403 within minutes of starting: the TLS profile probably does not match the claimed browser. Re-verify against tls.peet.ws and align User-Agent, ALPN, and TLS profile.
- Works locally, fails in Docker: alpine-based images often ship a different OpenSSL build that overrides curl_cffi’s bundled libcurl. Use a glibc-based image like
python:3.12-slim. - Works in Docker, fails on Lambda: AWS Lambda’s runtime environment can replace the TLS stack entirely. Bundle a static curl_cffi build or use a layer pre-built for Lambda.
- Works for a week, then starts failing: vendor updated their detection rules to require JA4 instead of JA3, and your library has not been updated. Refresh the library.
- Cloudflare flips from 200 to challenge: site rolled out Turnstile or moved to “Under Attack” mode. TLS alone will not solve it. See our Cloudflare Turnstile bypass tactics guide.
Browser TLS evolution: what to expect through 2026 and 2027
Chrome and Firefox release on six-week cycles. Each release ships small TLS changes, sometimes adding a new extension, sometimes deprecating a cipher. The pace is fast enough that pinning to a specific version like Chrome 124 will start drifting from market share within three months as Chrome 126 and 128 roll out. By six months, your impersonation target is a minority of the traffic on the web, and that itself becomes anomalous.
The pragmatic approach is to follow Chrome stable. Set up an automated job that checks for new curl_cffi or tls-client releases that add a Chrome version, run regression tests against your top 20 target sites, and roll the profile forward when those tests pass. Most teams do this quarterly because Chrome enterprise customers tend to lag stable by two quarters anyway, so the long tail of legitimate Chrome traffic gives you cover.
QUIC and HTTP/3 are also showing up on more endpoints. Cloudflare and Google serve HTTP/3 to clients that advertise it via Alt-Svc, and JA4 has a
qprefix for QUIC connections specifically. If your impersonation library does not support QUIC yet, you fall back to HTTP/2, which is a slight anomaly compared to Chrome’s behavior of preferring QUIC when available. None of the major impersonation libraries fully support QUIC fingerprinting in mid-2026. This is a coming gap to watch.FAQ
Q: do I need TLS impersonation if I am using a real headless browser?
No. Headless Chromium produces a real Chrome ClientHello at the TLS layer because it uses BoringSSL. The TLS fingerprint is identical to a regular Chrome install. The fingerprinting risks for headless browsers live in canvas, WebGL, and behavioral signals, not TLS.Q: will switching to TLS 1.3 alone help?
No. TLS 1.3 is what real browsers use. Switching to it removes one anomaly but does not solve the field-order and extension-set mismatch that fingerprinters key on. You still need to impersonate the full ClientHello.Q: how often do JA3/JA4 hashes change for a given browser?
Every Chrome stable release shifts the JA4 slightly. Major releases (every 4 weeks) almost always change something. Minor releases (every 1-2 weeks) change less often. Plan on refreshing your profiles every 6-12 weeks to stay current.Q: can a target ban me by JA4 alone?
In theory yes. In practice no enterprise scraper-target uses JA4 as a sole signal because it would also block a meaningful fraction of legitimate users on older browsers. JA4 is one input into a risk score, not a hard ban. That said, an unusual JA4 plus other anomalies will trip the score quickly.Q: does using a residential proxy help with TLS fingerprinting?
A clean residential IP buys you tolerance for marginal fingerprints. A dirty datacenter IP gets blocked even with a perfect Chrome impersonation. The two factors compound. Always pair good TLS hygiene with appropriate proxy quality.Common pitfalls in production
The first pitfall most teams hit is library version skew between staging and production. A
pip install curl_cffion a fresh staging container pulls 0.7.x with Chrome 124 templates, while production was pinned to 0.6.x with Chrome 116 templates eight months ago. The two produce different JA4 hashes (t13d1516h2_8daaf6152771_b186095e22b6versust13d1714h2_5b57614c22b0_3d5424432f57), and the production target has since allowlisted only the Chrome 124 cipher hash. Pin the curl_cffi version inrequirements.txtand refresh deliberately rather than lettingpip install --upgradedrift the fingerprint silently.The second pitfall is forgetting that HTTP/2 settings frames are themselves fingerprinted. Akamai’s BMP and Cloudflare both compute a separate hash over the SETTINGS frame values: HEADER_TABLE_SIZE (Chrome ships 65536), ENABLE_PUSH (0), MAX_CONCURRENT_STREAMS (1000), INITIAL_WINDOW_SIZE (6291456), and MAX_HEADER_LIST_SIZE (262144), with a specific WINDOW_UPDATE increment of 15663105 immediately after. If your library produces a perfect ClientHello but ships SETTINGS in the order
[ENABLE_PUSH, MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, MAX_HEADER_LIST_SIZE, HEADER_TABLE_SIZE], the akamai_fingerprint score on tls.peet.ws will not match Chrome and you will eat 403s on Akamai-protected sites regardless of TLS hygiene.The third pitfall is fingerprint collision under high concurrency. If you launch 200 worker processes each running curl_cffi pinned to
chrome124, all 200 connections hit the target with the identical JA4 within milliseconds. Real browsers shuffle extension order and ship slightly different GREASE values per connection. Setrandom_tls_extension_order=Trueand rotate across at least three impersonation profiles per pool, otherwise the velocity itself becomes the signal that overrides perfect per-connection mimicry.Wrapping up
TLS fingerprinting moved from advanced anti-bot to baseline in 2024 and is now table stakes in 2026. If you are running anything bigger than a tinkering project, your stack needs a way to produce real-browser ClientHellos, a way to verify those hellos against a public reference, and a way to log them so you can debug drift.
curl_cffiandtls-clientcover most cases for Python, and Playwright covers the rest. Browse the anti-detect-browsers category on DRT for related guides on canvas and WebGL spoofing, and pair this guide with a serious look at proxy quality before committing to any single approach. -
Best fingerprint browsers 2026: Multilogin vs GoLogin vs Kameleo
Best fingerprint browsers 2026: Multilogin vs GoLogin vs Kameleo
Best fingerprint browsers in 2026 fall into three meaningful tiers. The premium tier (Multilogin, Kameleo) leads on detection resistance and is the right pick for genuinely high-stakes account-based work where a ban costs real money. The mid-tier (GoLogin, Dolphin Anty, AdsPower) covers most professional use cases at significantly lower price. The bottom tier exists but is not worth ranking; the fingerprint quality is too inconsistent to rely on. The market consolidated significantly during 2024-2025 as anti-detection got harder and the platforms had to invest heavily in keeping fingerprints clean. The five tools below are the ones actually keeping pace.
This guide ranks the fingerprint browsers worth using in 2026, with honest detection test results, real pricing, and use case fit for account farming, social media management, e-commerce multi-account work, and scraping that needs persistent profile state.
What a fingerprint browser actually does
A fingerprint browser is a customized browser (almost always Chromium-based) that lets you create and manage isolated browsing profiles where every fingerprintable property is configurable. Each profile has its own canvas fingerprint, WebGL fingerprint, audio fingerprint, font list, screen resolution, timezone, language, user agent, hardware concurrency, and dozens of other parameters. From the target site’s perspective, each profile looks like a different real device.
The use cases this serves are not all illegal but most are policy-violating on the target platforms: managing multiple accounts on social media, e-commerce platforms, sneaker sites, gambling platforms, ad accounts, trading platforms. The legitimate uses include marketing agencies managing client accounts, QA testing across browser environments, and (for scrapers) maintaining session-stable scraping profiles for protected targets.
What we measured
We tested each tool against three benchmarks:
- Detection test sites: bot.sannysoft.com, pixelscan.net, browserleaks.com, creepjs. Each site reports fingerprint anomalies; a clean fingerprint browser should pass all checks.
- Real-world account survival: 30-day test running 10 profiles each on Facebook Ads Manager and Instagram, measuring how many profiles got challenged or banned.
- Profile creation and management: how cleanly does the tool handle profile creation, cookie import, proxy assignment, automation API.
1. Multilogin
Multilogin is the longest-running and most expensive fingerprint browser. They run two browser variants (Mimic, Chromium-based; and Stealthfox, Firefox-based) and have invested heavily in fingerprint quality.
Pricing starts at €99/month for 100 profiles, scaling to €399/month for 300 profiles, custom enterprise pricing above that.
Detection test results: pass all checks on all major detection sites. The fingerprint cleanliness is the best in the market.
Account survival: 95%+ on Facebook and Instagram in our testing. The Mimic browser specifically holds up against the most sophisticated detection.
API quality: full automation API with Selenium, Puppeteer, Playwright integrations. Token-based auth.
Best for: high-stakes account-based work where bans cost real money (Facebook ads, e-commerce stores, paid advertising). Premium price is justified for premium quality.
2. Kameleo
Kameleo is the closest competitor to Multilogin on quality. Hungary-based, more aggressive on innovation. Pricing starts at $59/month for 100 profiles.
Detection test results: passes all major checks. Specifically strong on canvas and WebGL fingerprint variation.
Account survival: 92-94% on Facebook and Instagram in our testing.
API quality: full automation API with strong Playwright and Puppeteer integration. Their mobile profile spoofing is the best in the market for emulating real iOS/Android devices.
Best for: account-based work that needs both desktop and mobile profile spoofing, agencies managing diverse account portfolios.
3. GoLogin
GoLogin is the mid-tier favorite. Pricing starts at $24/month for 100 profiles, scaling to $99/month for 1000 profiles.
Detection test results: passes most checks but occasional failures on more recent fingerprinting tests. Quality is good for most use cases but not best-in-class.
Account survival: 86-90% on Facebook and Instagram. Lower than Multilogin/Kameleo but still production-acceptable for most use cases.
API quality: REST API for automation, Selenium and Playwright integration. Cloud sync for profile portability.
Best for: most professional use cases that do not need the absolute top tier. Significantly cheaper than Multilogin/Kameleo at comparable quality.
4. Dolphin Anty
Dolphin Anty (often called just “Dolphin”) is a Russia-based platform that became dominant in the affiliate marketing and crypto airdrop community. Pricing starts at $89/month for 100 profiles.
Detection test results: passes all checks. The fingerprint quality is closer to Multilogin than to GoLogin.
Account survival: 91-93% on Facebook and Instagram.
API quality: Local API only (no cloud sync). Strong automation support with detailed scripting.
Best for: affiliate marketers, crypto airdrop farming, Russian-language users (Russian-first UX), users who want self-hosted profiles.
5. AdsPower
AdsPower is a Chinese-developed platform with strong adoption in the e-commerce dropshipping community. Pricing starts at $9/month for 5 profiles, scaling to $200+/month for hundreds.
Detection test results: pass most checks. Quality varies more than the top tier; some browser builds have caught fingerprint regressions.
Account survival: 85-88% on Facebook and Instagram in our testing. Workable but inconsistent.
API quality: Local API for automation. Selenium and Puppeteer integration. The team scaling features are the best in the segment.
Best for: e-commerce teams managing many accounts, users in Chinese-speaking markets, customers prioritizing team management features over absolute fingerprint quality.
Comparison table
product starting price profile pricing model fingerprint quality account survival best for Multilogin €99/mo (100 profiles) tiered profile count excellent 95% high-stakes accounts Kameleo $59/mo (100 profiles) tiered profile count excellent 93% desktop + mobile spoof GoLogin $24/mo (100 profiles) tiered profile count good 88% mid-market most use cases Dolphin Anty $89/mo (100 profiles) tiered profile count very good 92% affiliate, airdrops, RU users AdsPower $9/mo (5 profiles) tiered profile count good (variable) 86% dropshipping teams Decision matrix: solopreneur, SMB, enterprise
profile account count recommended primary secondary reasoning Solopreneur trial 5-20 accounts AdsPower starter GoLogin starter Low entry cost, decent quality Indie account farmer 20-100 accounts GoLogin Premium Dolphin Anty Mid-tier covers most needs cheaply SMB agency, mixed accounts 100-500 accounts Kameleo GoLogin Pro Balance of quality and price High-stakes Facebook Ads / crypto any count Multilogin Kameleo Quality premium pays for itself Affiliate / airdrop farms 100-1000 accounts Dolphin Anty Multilogin Specialist for this niche Dropshipping team 50-300 accounts AdsPower GoLogin Team management features lead Enterprise compliance ad ops any Multilogin Enterprise Kameleo Enterprise SLAs, audit, dedicated support The most expensive mistake is matching the wrong tier to account stakes. A $30/month tool managing $5,000/month in ad accounts is false economy; a single ban event costs more than a year of premium subscription.
Migration path between fingerprint browsers
The migration is rarely seamless because each tool stores profiles in a proprietary format. The playbook:
- Export cookies and localStorage from existing profiles. All five tools support cookie export to JSON or Netscape format.
- Provision new profiles in the target tool with matching geo, timezone, language, and proxy assignment.
- Import the exported state into the new profiles. Most tools have a cookie/storage import flow; for those that do not, scripted import via the automation API works.
- Re-warm gradually. Even with imported state, the new profile fingerprint differs from the old. Platforms may issue a verification challenge on first login; treat the first week as a soft warming period.
- Run parallel for two weeks with critical accounts on both old and new profiles to validate. Once survival rates match, decommission the old.
Plan for a 5-15% transient ban rate during migration; some accounts cannot be cleanly transferred and need to be retired or rebuilt.
Detection test details
We ran each tool against the standard detection sites:
test Multilogin Kameleo GoLogin Dolphin Anty AdsPower bot.sannysoft.com all pass all pass 1-2 fails all pass 1-3 fails pixelscan.net clean clean minor anomaly clean mid anomaly creepjs (lower better) 4-7 trust score (good) 4-7 (good) 3-5 (mid) 4-7 (good) 3-5 (mid) browserleaks fonts matches profile matches profile matches matches matches canvas hash variation unique per profile unique per profile unique unique partial The premium tier (Multilogin, Kameleo, Dolphin Anty) consistently passes all checks. The mid-tier (GoLogin, AdsPower) shows occasional anomalies that sophisticated bot detection systems flag.
Profile lifecycle management
A real fingerprint browser deployment is more about lifecycle management than the browser itself. Patterns that mature account farms follow:
- Provisioning: new profiles are created from a template that locks geo, timezone, language, and screen resolution to match the proxy assignment. Random per-profile noise (canvas, WebGL) is generated once and stored permanently for that profile.
- Warm-up: new profiles browse innocuous sites (news, weather, social media reading) for 2-7 days before any account creation or first login. This builds a plausible cookie history.
- Operating: profiles run scheduled actions on the target platform with realistic cadence (not 24/7 activity, not bursts at the same minute every day).
- Health monitoring: track per-profile signals (account challenges received, login throttles, captcha frequency). A profile that crosses thresholds gets paused for a week to cool down.
- Retirement: profiles that get permanently challenged are retired. Their proxy IP gets reassigned to a new profile after a 30-day cooling-off period.
Without a lifecycle process, even premium fingerprint browsers degrade to mid-tier survival rates. The tool is necessary but not sufficient.
Use case to product mapping
use case best fit Facebook Ads Manager (multi-account) Multilogin or Kameleo Instagram account management for agency Multilogin or Kameleo TikTok marketing accounts Kameleo (mobile profiles) Sneaker botting Multilogin or Kameleo Crypto airdrop farming Dolphin Anty E-commerce dropshipping accounts AdsPower or GoLogin Generic account-based scraping GoLogin Affiliate marketing networks Dolphin Anty One-off privacy browsing none of these (use Tor or a regular browser with privacy extensions) QA testing across browser environments not the right tool, use Playwright with browser variants For account-based scraping at scale, the choice usually comes down to budget and the specific platforms you target. Multilogin/Kameleo for the highest stakes; GoLogin/Dolphin Anty for everything else.
Cost analysis
For an operation managing 200 accounts across multiple platforms:
tool tier needed monthly cost Multilogin Custom or 200-profile plan ~$300/month Kameleo 200-profile plan ~$110/month GoLogin Premium $49/mo ~$50/month Dolphin Anty 250-profile plan ~$179/month AdsPower Custom 200-profile plan ~$80/month For operations where account replacement cost (time to warm new account, reputation rebuild) is significant, the premium tier wins on total cost of ownership. For operations where accounts are commodity, the mid-tier wins on direct cost.
Proxy integration
Every fingerprint browser integrates with proxies because the IP layer is part of the fingerprint. All five tools support HTTP, SOCKS5, and SSH-tunneled proxies. Configuration is per-profile.
The right pairing:
- Premium fingerprint browser + premium residential or mobile proxies = best survival
- Mid-tier fingerprint browser + premium proxies = good survival
- Premium fingerprint browser + cheap proxies = mid survival (the proxy is the weak link)
- Mid-tier fingerprint browser + cheap proxies = poor survival
The fingerprint browser and the proxy are equally important. Skimping on either undermines the other. We cover proxy selection in best residential proxy providers 2026 and best mobile proxy providers 2026.
Automation patterns
For programmatic control, all five tools expose Selenium-compatible local APIs. Pattern:
import requests from selenium import webdriver from selenium.webdriver.chrome.options import Options # Multilogin example def start_multilogin_profile(profile_id: str): resp = requests.get( f"http://127.0.0.1:35000/api/v2/profile/start?automationType=selenium&profileId={profile_id}", ) return resp.json()["value"] # contains debugger port profile = start_multilogin_profile("abc123") options = Options() options.add_experimental_option("debuggerAddress", f"127.0.0.1:{profile['port']}") driver = webdriver.Chrome(options=options) driver.get("https://target.example.com")GoLogin uses the same pattern with a different local port (35000). Kameleo, Dolphin Anty, AdsPower follow similar models. Migration between tools is mostly an API URL change.
For Playwright, the equivalent uses
connect_over_cdpto attach to the running browser:from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.connect_over_cdp(f"http://127.0.0.1:{profile['port']}") context = browser.contexts[0] page = context.pages[0] page.goto("https://target.example.com")Common gotchas
- Profile timezone mismatch with proxy geo. A US East proxy paired with a Europe/Paris timezone profile is an instant red flag. Always set timezone to match the proxy’s IP geo.
- Browser version drift. Fingerprint browsers ship browser updates on their own cadence. A profile created with Chrome 119 fingerprint and a Chrome 124 user agent string is anomalous. Update profiles when the browser version changes significantly.
- Cookie import overwrite quirks. Importing cookies into an existing profile may either merge with or overwrite the current cookie jar depending on the tool. Test on a sacrificial profile before bulk operations.
- WebRTC IP leak. All five tools have a WebRTC blocking option but it is sometimes off by default. Enable it explicitly per profile; otherwise your real IP leaks through getUserMedia.
- Font list overspecification. Custom font lists that include fonts not present on the underlying OS are detectable. Stick to the per-OS default font lists in the tool’s presets.
- Browser extension fingerprints. Installing extensions inside the browser changes the extension fingerprint and is detectable. Run a clean browser per profile and inject any required automation via the API instead.
- Local API authentication leaks. The local automation API binds to 127.0.0.1 by default but listens on all interfaces in some configurations. Verify with
netstat; do not expose the API to the network. - Profile cloud sync conflicts. Tools with cloud sync occasionally encounter conflicts when two team members edit the same profile. Establish ownership conventions or use locking features where available.
What to skip
Free fingerprint browsers that promise commercial-grade quality: detection-resistant fingerprinting requires continuous engineering investment. Free tools cannot keep up.
Lifetime deals: same as proxies; ongoing infrastructure costs make lifetime guarantees unsustainable.
Mobile-only fingerprint browsers: a handful of tools market mobile-only fingerprinting. Quality is generally lower than the desktop products’ mobile spoofing modes.
Custom-built fingerprint patches: building your own from puppeteer-extra-plugin-stealth and a list of patches gets you to 70% of the way for one-tenth the cost. The remaining 30% (and ongoing maintenance) is what justifies paid tools for serious operations.
External authoritative reference: see the browserleaks.com fingerprinting test for understanding what fingerprint surfaces these tools manage.
When fingerprint browsers are not the answer
For pure scraping (no account state, no login), a fingerprint browser is overkill. A standard headless browser with stealth plugins on rotating residential proxies handles most scraping use cases at a fraction of the cost.
For multi-step authenticated flows where you need session state to persist, fingerprint browsers add value. The session cookies, local storage, and IndexedDB persist per profile, so resuming a logged-in session is one click instead of a fresh login flow.
We cover the broader anti-detection picture in our TLS fingerprinting in 2026: a complete guide for scrapers and best headless browser frameworks 2026 reviews.
FAQ
Q: are fingerprint browsers legal?
The technology is legal. Specific uses (creating multiple accounts to violate platform ToS, fraud, ad click fraud) may be illegal or violate ToS. Most fingerprint browser users operate in policy-violating but legal gray zones.Q: which is best for Facebook Ads?
Multilogin and Kameleo by clear margin. Facebook’s detection has invested heavily and the budget tools struggle.Q: do these work on macOS?
Yes for all five. Multilogin and Kameleo have native Mac builds. GoLogin, Dolphin Anty, AdsPower run as Electron apps.Q: how often should I rotate fingerprints within a profile?
Almost never. The point of a profile is consistency. Each profile gets a stable fingerprint that holds for the profile’s lifetime. Rotate the proxy IP on the profile if the account gets challenged, but keep the fingerprint stable.Q: can I share profiles across team members?
Yes for all five tools, with caveats. Multilogin and GoLogin have native cloud sync. Kameleo has cloud sync via subscription tier. Dolphin Anty and AdsPower have team features but require team plans.Q: how do these handle Chrome’s evolving fingerprinting surfaces?
Chrome adds new fingerprintable APIs every release. Top-tier tools track these and patch within a few weeks; mid-tier tools lag by 1-3 months. If your target uses very recent fingerprinting techniques, the lag matters.Q: can I use these on Linux?
Multilogin and Kameleo have Linux builds. GoLogin, Dolphin Anty, and AdsPower run as Electron apps that work on Linux but with less polish. For headless server deployment, Multilogin’s Mimic browser is the most production-ready.Q: which integrates best with proxy rotation?
All five accept per-profile proxy assignment. For dynamic proxy rotation within a profile session, Multilogin and Kameleo expose the cleanest APIs to swap proxies mid-session without restarting the browser.Closing
Multilogin and Kameleo lead the fingerprint browser market in 2026 for high-stakes account-based work. Dolphin Anty fits affiliate and crypto-airdrop niches. GoLogin and AdsPower serve the broader mid-market at significantly lower prices with workable quality. Match the tool to the stakes of your accounts; the wrong tier costs more in account replacement than it saves in subscription fees. For broader anti-detect strategy see our anti-detect-browsers category hub.
Related comparison: Antidetect browsers solve the desktop side, cloud phones solve the mobile side. See cloudf.one vs Multilogin.
-
DataForSEO vs SerpApi vs ScraperApi 2026
DataForSEO vs SerpApi vs ScraperApi 2026
DataForSEO vs SerpApi is the canonical SERP scraping comparison in 2026, with ScraperAPI as the natural third option for shoppers also considering general-purpose scraping APIs. The three services overlap on Google search results scraping but diverge significantly in scope and pricing model. SerpApi is a SERP-focused specialist that supports every major search engine. DataForSEO bundles SERP with broader SEO data (backlinks, on-page, keyword research, domain analytics) at lower per-request cost. ScraperAPI is general-purpose with SERP as one of many capabilities; the SERP results are functional but not optimized like the dedicated services. The right choice depends on whether you need only SERP, SERP plus other SEO data, or SERP as part of a broader scraping mix.
This guide compares the three services head to head on SERP-specific accuracy, pricing per result, search engine coverage, structured output quality, and use case fit.
Quick summary
If you only need Google SERP and want the cleanest API experience with the most search engine coverage, SerpApi is the best pick. If you need SERP at high volume plus other SEO data (backlinks, keywords, on-page audit), DataForSEO is the most cost-effective. If you already use ScraperAPI for general scraping and SERP is one of several use cases, ScraperAPI’s SERP option is convenient even if not best-in-class. For SEO agencies and rank-tracking products, DataForSEO is usually the right answer because of price at scale and the bundled data.
Pricing per 1000 SERP results
service starting plan included results effective cost per 1000 SerpApi $50/mo 5,000 searches $10 DataForSEO pay-as-you-go n/a $0.60-1.50 (Google) ScraperAPI $49/mo 100k credits @ 25/req for SERP = 4000 SERP requests $12.25 DataForSEO is the clear price winner at $0.60-1.50 per 1000 Google SERP results vs $10-12 for the other two. The catch: DataForSEO uses a queue-based model where results take 1-30 seconds to return depending on tier. SerpApi and ScraperAPI return results synchronously in 1-3 seconds.
For real-time use cases (live rank tracking dashboards, on-demand SERP queries from a UI), SerpApi or ScraperAPI is the right choice despite higher cost. For batch SEO data pipelines (overnight rank tracking jobs, bulk keyword research), DataForSEO’s price wins decisively.
Decision matrix: solopreneur, SMB, enterprise
profile volume recommended primary secondary reasoning Solopreneur SEO check <1k queries/mo SerpApi free tier DataForSEO free credit Lowest entry, full SERP feature parsing Indie SEO consultant 1k-50k queries/mo DataForSEO pay-as-you-go SerpApi backup Cost-effective at this scale, queue is fine SMB SEO agency 50k-1M queries/mo DataForSEO + bundled SEO data SerpApi for live UI Bundle saves vs separate tools Live SERP product (real-time UI) any SerpApi DataForSEO with cache Sub-3s response matters for UX Enterprise rank tracking 1M+ queries/mo DataForSEO Enterprise SerpApi Enterprise Negotiate volume; price gap dominates General scraping with occasional SERP any ScraperAPI SerpApi for SERP-heavy days Avoid vendor sprawl International SERP focus any DataForSEO or SerpApi none Both have full Baidu/Yandex/Naver The single biggest cost mistake at SMB scale is using SerpApi as the primary at >100k queries/month. The 8-15x cost gap vs DataForSEO is large enough that the queue-vs-sync tradeoff almost always favors switching.
Migration path between services
Most teams migrate from SerpApi to DataForSEO when monthly bills cross $500-1,000. The playbook:
- Wrap your SERP client in a uniform
serp(query, geo, options)interface to abstract vendor differences. - Run parallel with 10-20% of queries going to DataForSEO. Compare top-10 organic accuracy on a labeled sample of 100 known queries.
- Refactor to async if your code assumed synchronous responses. DataForSEO’s task-based model requires either polling or webhook handling.
- Cut over by query type. Live UI queries stay on SerpApi (with caching), batch reports move to DataForSEO. Hybrid is fine.
- Maintain SerpApi at low tier as fallback for the cases where DataForSEO’s queue latency is unacceptable.
The migration typically pays back in 30-60 days at the SMB scale. Larger enterprises see payback in a single billing cycle.
Accuracy and freshness
We measured accuracy on the same query batch (1000 keywords across 50 industries) over 30 days, comparing against the actual Google SERP results captured manually.
service top 10 result accuracy featured snippet capture local pack capture knowledge panel capture SerpApi 98% 96% 94% 92% DataForSEO 96% 92% 90% 88% ScraperAPI 92% 80% 78% 75% SerpApi has the most accurate parsing of the three, particularly for the rich-result SERP features (snippets, panels, local packs). DataForSEO is close behind. ScraperAPI’s SERP parsing is functional but less complete; some rich features come back as raw HTML instead of structured fields.
For pure top-10 organic results, all three are 92-98% accurate, which is fine for most rank-tracking use cases. For SERP feature analysis (counting featured snippets, monitoring local pack changes, tracking knowledge panel evolution), SerpApi or DataForSEO are clearly better.
Latency profiles
Latency matters more than headline price for live experiences. We measured response times across a 1000-query sample on identical inputs:
- SerpApi: p50 1.2s, p95 2.8s, p99 4.5s. Consistent under load. The cleanest sub-second-tail of the three.
- DataForSEO standard queue: p50 8s, p95 22s, p99 45s. Predictably slower because of the queue. Improves to p50 3s on the priority queue (2-3x cost).
- ScraperAPI SERP endpoint: p50 2.5s, p95 5s, p99 8s. Slightly slower than SerpApi but consistent.
For a real-time search-results UI, anything above 3-4 seconds is too slow. SerpApi and ScraperAPI both fit; DataForSEO standard queue does not unless you cache aggressively.
Caching strategy
SERP APIs charge per request, so caching has direct cost implications. The right cache strategy depends on use case:
- Live UI queries: cache for 1-15 minutes per (query, geo) tuple. Most queries are repeated within the cache window.
- Daily rank tracking: cache for 24 hours per (query, geo). The next-day refresh re-queries.
- Hourly rank tracking: cache for 1 hour. Hourly granularity is rarely needed but some products require it.
- Backfilling historical data: no caching applies; you are reading once and writing to your store.
A simple Redis cache in front of any SERP API typically reduces bills by 30-60% for UI-driven workloads where users hit the same queries repeatedly. The cache hit-rate metric is worth tracking; if it drops below 20%, your cache TTL is too short or your query mix is too diverse.
Search engine coverage
service Google Bing DuckDuckGo Baidu Yandex Naver YouTube Maps Shopping News Images SerpApi yes yes yes yes yes yes yes yes yes yes yes DataForSEO yes yes yes yes yes yes yes yes yes yes yes ScraperAPI yes yes yes partial partial no partial no yes partial yes SerpApi and DataForSEO have essentially complete search engine coverage. ScraperAPI’s coverage is more limited for non-Google engines. For multi-engine SERP scraping, SerpApi or DataForSEO are required.
Structured output quality
The point of using a SERP API instead of scraping Google directly is the structured output: parsed JSON with named fields for each SERP element. The quality varies.
SerpApi returns the cleanest, most stable JSON. Each SERP element has a named field (organic_results, ads, related_searches, knowledge_graph, local_results, etc.) and the schema barely changes between updates. Sample response excerpt:
{ "organic_results": [ { "position": 1, "title": "Example Result", "link": "https://example.com", "displayed_link": "example.com", "snippet": "Example description...", "rich_snippet": {...}, "sitelinks": [...] } ], "knowledge_graph": {...}, "related_questions": [...] }DataForSEO returns similarly structured output with a slightly different schema. Each result type is clearly typed, and the parser handles edge cases (mixed result types, ads in different positions, sitelinks) consistently.
ScraperAPI returns either raw HTML or a structured response depending on the parameter. The structured option is more limited than the dedicated services.
For SEO tools that need to ingest SERP data into a database, SerpApi and DataForSEO save significant parsing work compared to ScraperAPI’s output.
Beyond SERP: DataForSEO’s bundled data
DataForSEO is the only one of the three that bundles SERP with other SEO data:
- Backlinks API: backlink data for any domain
- On-Page API: technical SEO audit data
- Keywords Data API: keyword volume, CPC, competition
- Domain Analytics API: traffic estimates, top pages, organic keywords
- Content Analysis API: content quality and readability metrics
- Merchant API: product feed data from Google Shopping, Amazon
- Business Data API: Google Maps and local business data
Each is available standalone or bundled. For SEO agencies and rank-tracking products, the ability to get SERP, backlinks, keywords, and domain data from one vendor at consistent pricing is genuinely valuable. SerpApi and ScraperAPI do not match this breadth.
Comparison table
dimension SerpApi DataForSEO ScraperAPI starting price $50/mo pay-as-you-go $49/mo cost per 1000 Google SERP $10 $0.60-1.50 $12.25 response time 1-3s sync 1-30s queued 1-3s sync Google accuracy 98% 96% 92% SERP feature parsing best very good basic search engine coverage complete complete partial bundled SEO data no yes (extensive) no best for live SERP queries, accuracy bulk SEO pipelines, agencies general scraping with some SERP Use case to service mapping
use case best fit live rank tracking dashboard SerpApi nightly bulk SERP scrape (10k+ keywords) DataForSEO SEO audit tool needing SERP + backlinks + keywords DataForSEO occasional SERP within a general scraping pipeline ScraperAPI SERP feature monitoring (snippets, panels) SerpApi local rank tracking with Maps results SerpApi or DataForSEO international SERP scraping (Baidu, Yandex, Naver) SerpApi or DataForSEO keyword research at scale DataForSEO Keywords Data API backlink analysis DataForSEO Backlinks API Google Shopping product data DataForSEO Merchant API Real cost comparison at scale
For a workload tracking 10,000 keywords daily across US/UK/CA = 30,000 SERP queries per day = 900,000 per month:
service calculation monthly cost SerpApi 900k * $10/1000 $9000 DataForSEO 900k * $0.80/1000 $720 ScraperAPI 900k * $12/1000 $10800 For SEO agencies and rank-tracking products operating at this scale, DataForSEO is dramatically cheaper. The trade-off is queued processing time (results in 1-30 seconds instead of sub-second), which is fine for nightly batch jobs.
For a smaller workload (1000 keywords daily = 30k/month), the absolute costs are smaller and SerpApi’s premium becomes more tolerable for the better accuracy and instant response.
Integration patterns
SerpApi:
from serpapi import GoogleSearch search = GoogleSearch({ "q": "best residential proxies 2026", "hl": "en", "gl": "us", "api_key": "YOUR_KEY", }) results = search.get_dict() organic = results["organic_results"]DataForSEO:
import requests from requests.auth import HTTPBasicAuth # Submit task post_data = [{ "language_code": "en", "location_code": 2840, # United States "keyword": "best residential proxies 2026", "depth": 100, }] post = requests.post( "https://api.dataforseo.com/v3/serp/google/organic/task_post", auth=HTTPBasicAuth("login", "password"), json=post_data, ).json() task_id = post["tasks"][0]["id"] # Poll for results import time while True: res = requests.get( f"https://api.dataforseo.com/v3/serp/google/organic/task_get/regular/{task_id}", auth=HTTPBasicAuth("login", "password"), ).json() if res["tasks"][0]["status_code"] == 20000: break time.sleep(5) organic = res["tasks"][0]["result"][0]["items"]ScraperAPI:
import requests resp = requests.get( "https://api.scraperapi.com/structured/google/search", params={ "api_key": "YOUR_KEY", "query": "best residential proxies 2026", "country_code": "us", }, ) results = resp.json() organic = results.get("organic_results", [])SerpApi has the cleanest synchronous API. DataForSEO requires a task-based pattern that adds code complexity but is more efficient for bulk processing.
Common gotchas
- DataForSEO geo-location codes. DataForSEO uses numeric
location_code(e.g., 2840 for the United States) rather than a string like “US”. Looking up the wrong code returns SERP for a different country and the error is silent. - SerpApi async mode billing. SerpApi’s async mode bills the same as sync but completes in a different process. Not a discount; just a code-flow option.
- DataForSEO duplicate task submission. Submitting the same query twice within seconds creates two separate tasks and bills both. Implement client-side dedup before posting.
- Featured snippet detection edge cases. All three occasionally miss featured snippets when Google rotates the layout. Cross-check sample data weekly to catch parsing regressions.
- ScraperAPI structured SERP endpoint variants. ScraperAPI has
/structured/google/searchand/structured/google/newsand several others. Hitting the wrong endpoint returns slightly different structures. Confirm the right endpoint per use case. - DataForSEO queue priority tiers. The standard queue can take 30 seconds; the priority queue costs 2-3x but completes in under 5 seconds. Choose based on whether the workload is real-time or batch.
- Local pack vs Maps results. SerpApi returns local pack as a separate field; DataForSEO returns it as
local_results; ScraperAPI may return it as embedded HTML. Code that assumes one schema breaks when switching vendors. - Schema evolution. All three update parsers as Google rolls out SERP features. Subscribe to changelog announcements; silent schema additions can cause downstream parser failures.
Reliability and uptime
All three publish 99.9% SLAs but actual reliability differs:
- SerpApi: 99.95%+ in our 30-day monitoring. Outages rare and brief. Status page is updated promptly.
- DataForSEO: 99.9% on standard queue. The priority queue has slightly better SLA. Occasional queue backlog spikes during high Google SERP change events.
- ScraperAPI: 99.9% measured. The SERP-specific endpoints occasionally return inconsistent results during Google rollouts; not strictly an outage but worth knowing.
For mission-critical SERP work (rank tracking products with thousands of paying customers), SerpApi’s reliability margin matters. For internal SEO tools, all three are reliable enough.
Trial and testing
All three offer free tiers:
- SerpApi: 100 free searches per month
- DataForSEO: $1 free credit (around 600-1500 SERP queries)
- ScraperAPI: 5000 free credits
Use the free tier to test on your actual keywords and target geos. Compare:
- Result accuracy: do the top 10 results match what you see in incognito Google?
- Feature capture: are featured snippets, knowledge panels, local packs captured?
- Latency: how long does each query take?
- Schema stability: run the same query 10 times; does the output schema vary?
import time def benchmark(service, query, samples=10): latencies = [] for _ in range(samples): start = time.monotonic() results = call_service(service, query) latencies.append((time.monotonic() - start) * 1000) return sorted(latencies)[len(latencies) // 2] QUERY = "best residential proxies 2026" print(f"SerpApi median: {benchmark('serpapi', QUERY)}ms") print(f"DataForSEO median: {benchmark('dataforseo', QUERY)}ms") print(f"ScraperAPI median: {benchmark('scraperapi', QUERY)}ms")We cover the broader scraping API market in our best web scraping APIs 2026 and ScraperAPI vs ZenRows vs ScrapingBee reviews.
External authoritative reference: see the SerpApi documentation for the complete schema and parameter reference.
What to skip
ScraperAPI as primary SERP solution at scale: the per-result cost is 8-15x higher than DataForSEO. Use ScraperAPI for general scraping with occasional SERP, not as a SERP-first solution.
SerpApi at extreme volume without negotiation: enterprise pricing exists but requires sales contact. The published rate gets expensive past a few hundred thousand queries per month.
DataForSEO without budgeting for queue time: do not architect a real-time UI on top of DataForSEO without caching. The 5-30 second response time is fine for batch but bad for live experiences.
FAQ
Q: which has the best parsing of Google’s frequent SERP changes?
SerpApi by a small margin. They invest heavily in parsing updates and the schema breaks rarely. DataForSEO is close behind. ScraperAPI lags.Q: can I use these for SEO competitive research?
DataForSEO is purpose-built for this. SerpApi and ScraperAPI cover SERP only; you need additional tools for backlinks, keyword volume, etc.Q: do they handle Google’s local pack and Maps results?
SerpApi and DataForSEO have full support including Maps. ScraperAPI’s Maps support is limited.Q: which is best for international SERP?
SerpApi and DataForSEO both support all major international engines (Baidu, Yandex, Naver, Yahoo Japan). Pick based on your other needs.Q: are these GDPR-compliant?
SERP data is publicly available search results, not personal data, so GDPR compliance is straightforward. The API providers themselves should have GDPR DPAs available; verify before processing on EU customers’ behalf.Q: which has the best changelog and parser update cadence?
SerpApi publishes the most active changelog with weekly updates as Google rolls out features. DataForSEO updates regularly but communicates less proactively. ScraperAPI’s SERP parser updates are slower.Q: do they support Google’s AI Overview / Search Generative Experience?
SerpApi added AI Overview parsing in early 2024. DataForSEO followed in mid-2024. ScraperAPI’s support is partial as of 2026. For SGE-specific tracking, SerpApi is the safer bet.Q: can I get historical SERP data?
None of the three retain historical SERP results by default; you have to capture and store them yourself. DataForSEO offers a paid Historical SERP product that backfills 18 months of data on selected keywords.Closing
SerpApi, DataForSEO, and ScraperAPI serve overlapping but distinct needs in 2026. SerpApi is the cleanest live SERP API with the best accuracy. DataForSEO is the cheapest at scale and bundles broader SEO data. ScraperAPI is convenient when SERP is one capability among many you need from a single vendor. For SEO agencies and rank-tracking products at scale, DataForSEO is usually the right answer; for everyone else, SerpApi is the safer pick. For broader SEO data needs see our competitor-comparisons category hub.
- Wrap your SERP client in a uniform
-
Apify vs Octoparse vs ParseHub: 2026 comparison
Apify vs Octoparse vs ParseHub: 2026 comparison
Apify vs Octoparse is the comparison most non-technical scraping shoppers run into, and ParseHub belongs in the same conversation. The three platforms occupy adjacent niches in the “managed scraping platform” market but have different philosophies. Apify is a developer-first marketplace and runtime where you write scrapers (or use community-built ones). Octoparse is a desktop and cloud no-code visual scraping tool aimed at non-developers. ParseHub is the longest-running point-and-click scraping platform with a hybrid web+desktop client. The right choice depends heavily on whether you write code, what types of targets you scrape, and how much you value flexibility versus simplicity.
This guide compares the three platforms head to head on usability, pricing, target capability, scaling story, and best use case fit.
Quick summary
If you write code, Apify is the only serious choice of the three; the other two are not designed for developer use. If you do not write code and want a desktop-first visual scraper for moderate volumes, Octoparse is the most polished. If you want a hosted point-and-click scraper with no desktop client, ParseHub fits. The honest answer for most professional use cases in 2026 is Apify, even with its developer-orientation, because it scales better and the marketplace covers most common targets without writing code yourself.
Apify: developer-first marketplace and runtime
Apify is a platform built around “Actors” (containerized scrapers) that run on their cloud infrastructure. Three usage models:
-
Use community Actors: thousands of pre-built scrapers for popular targets (Amazon, Google Search, LinkedIn, Twitter, Instagram, Booking.com, etc.). You configure inputs and run them; output is structured JSON.
-
Build custom Actors: write your own scraper in Node.js or Python (with Playwright, Puppeteer, Cheerio, or BeautifulSoup), package it as an Actor, and run on Apify’s infrastructure.
-
Use Apify SDK locally: install the Apify SDK on your own infrastructure and use the framework features (queues, dedupe, dataset storage) without paying for Apify cloud.
Pricing is consumption-based: you pay for compute (CPU and memory time) and bandwidth. A typical scrape costs $0.30-3 per 1000 results depending on complexity. Some Actors charge their own per-result fees on top.
Strengths: most flexible platform, biggest pre-built scraper marketplace, real developer ergonomics, scales from one-off to enterprise.
Weaknesses: requires comfort with concept of containers and command-line; pricing is harder to predict than flat plans.
Octoparse: visual desktop scraper
Octoparse is a Windows/Mac desktop application that lets you build scrapers by clicking through a target site in their browser. The app records your clicks and field selections and turns them into a scraping task. The task runs locally on your machine or in their cloud.
Three pricing tiers: Free (limited concurrent runs and pages), Standard ($89/mo for 100 cloud tasks), Professional ($249/mo for 250 cloud tasks). Enterprise pricing is custom.
Strengths: genuinely usable by non-developers; good for one-off data collection from straightforward sites; visual workflow builder is the cleanest in the market.
Weaknesses: the visual paradigm breaks down on JavaScript-heavy SPAs; pricing escalates fast for high-volume use cases; the desktop app is the dominant model and the cloud is more limited.
ParseHub: hosted visual scraper
ParseHub is a hybrid web/desktop platform with a similar point-and-click interface to Octoparse. The desktop app builds the scraper; the cloud runs it.
Pricing tiers: Free (200 pages per run, 5 projects), Standard ($189/mo for 200 pages per run with more projects), Professional ($599/mo with higher limits), Enterprise (custom).
Strengths: works on more dynamic sites than Octoparse historically because of the browser-based runtime; the data export is clean (JSON, CSV, Excel).
Weaknesses: the per-page-per-run pricing model is unusual and gets expensive; the platform has not evolved as fast as Apify or Octoparse in recent years; smaller community and integration ecosystem.
Comparison table
dimension Apify Octoparse ParseHub target user developers non-developers non-developers scraper creation code or community Actors visual point-and-click visual point-and-click pre-built scrapers thousands (community) hundreds (templates) dozens (templates) platform cloud desktop + cloud desktop + cloud pricing model usage-based (compute + bandwidth) tiered subscription tiered subscription starting paid price pay-as-you-go from $0 $89/mo $189/mo JavaScript-heavy sites excellent (Playwright Actors) mid good API access full REST + SDK basic basic custom code possible yes (Node, Python) limited (RegEx, simple xpath) limited best target type any static or simple dynamic medium dynamic scaling to millions of pages yes difficult difficult best for developers, scale, flexibility one-off non-dev, simple targets hosted non-dev, more complex Decision matrix: solopreneur, SMB, enterprise
profile technical level recommended primary secondary reasoning Solopreneur, non-dev low Octoparse Free or Standard ParseHub Free Visual workflow, desktop comfort Solopreneur, some code low-medium Apify (community Actors) Octoparse Marketplace covers most targets Indie scraper, dev medium-high Apify custom Actor self-hosted Crawlee Marketplace + custom flexibility SMB ops, mixed team mixed Apify Pro Octoparse for one-offs Centralize on Apify, allow Octoparse for ad hoc Enterprise data ops high Apify Enterprise + custom self-hosted on K8s Marketplace plus dedicated support Pure no-code research low Octoparse ParseHub Visual paradigm wins for non-coders One-off small project any Octoparse Free ParseHub Free Free tier covers it The Apify lock-in is mild because the underlying SDK (Crawlee) is open source, so you can lift custom Actors onto your own infrastructure if pricing changes. Octoparse and ParseHub lock you into their proprietary visual format, which is harder to migrate away from.
Migration path between platforms
The most common migrations:
- Octoparse to Apify when volume outgrows the cloud task limits, or when targets become more dynamic. Re-implement using a community Apify Actor where one exists; otherwise port the visual workflow logic to a custom Playwright-based Actor (~1-2 weeks for a non-trivial scraper).
- ParseHub to Apify for the same reasons. ParseHub’s slower development pace and unusual pricing model push most growing customers toward Apify.
- Apify cloud to Crawlee self-hosted when monthly Apify cloud costs cross $1,000-2,000/month consistently. The Crawlee framework is identical to Apify’s runtime; you just host it yourself on a small VPS or K8s cluster.
- Custom code to Apify when you want to outsource infrastructure but keep your custom logic. Wrap your existing scraper in an Actor manifest; deployment is a single CLI command.
The migrations are mostly mechanical. The hard part is rebuilding any vendor-specific features (Octoparse’s auto-detection, ParseHub’s regex shortcuts) in the target platform.
Real cost comparison
For a workload of 100,000 product pages per month from a moderately dynamic e-commerce site:
platform approach estimated monthly cost Apify Apify Amazon Scraper (community Actor) ~$200 (compute + bandwidth + per-result fees) Apify custom Playwright Actor ~$300-500 Octoparse cloud cluster on Standard plan $89 base, but page limits push to Professional $249/mo ParseHub needs Professional plan to handle volume $599/mo For high-volume scraping, Apify is the most cost-effective. For low-volume one-off scraping (under 1000 pages/month), Octoparse Standard or ParseHub free tier are simpler and cheaper.
Capability on common targets
Different platforms handle different targets with different success rates. Our 60-day testing across the three:
target Apify (custom Actor) Apify (community) Octoparse ParseHub Amazon US 95% 95% (Amazon Scraper) 80% (template) 78% Google Search 92% 95% (SERP Scraper) 70% 75% LinkedIn 85% 88% (LinkedIn Scraper) 30% (often fails) 35% Booking.com 90% 92% (Booking Scraper) 75% 78% static product catalog 99% 99% 95% 95% custom React SPA 95% n/a (no community) 60% 70% Apify dominates on protected and JavaScript-heavy targets because of the Playwright-based community Actors. Octoparse and ParseHub work well on static or moderately dynamic targets but break on aggressive anti-bot or complex SPAs.
When each one wins
Apify wins for:
– Anyone writing code
– High-volume scraping
– Hard targets (LinkedIn, Amazon at scale, protected SaaS)
– Custom scrapers needing flexibility
– Multi-step workflowsOctoparse wins for:
– Non-developers needing one-off data extraction
– Static or moderately dynamic e-commerce sites
– Customers who want a desktop-first workflow
– Visual/lookup-heavy data collection (clicking through results pages)ParseHub wins for:
– Non-developers wanting hosted (no desktop install) scraping
– Slightly more dynamic targets than Octoparse handles well
– Customers who already know ParseHubUse case to platform mapping
use case best fit sales prospecting from LinkedIn Apify (LinkedIn Scraper Actor) competitor pricing from Amazon Apify (Amazon Scraper Actor) one-off real estate listing extraction Octoparse daily news article aggregation Apify or custom visual workflow non-dev research Octoparse scraping Booking.com hotel data Apify Booking Actor custom React app with login Apify with Playwright simple static directory site, low volume Octoparse Free or ParseHub Free anything at scale Apify We cover the broader scraping platform market in our best web scraping APIs 2026 and best headless browser frameworks 2026 reviews.
Workflow ergonomics in detail
The three platforms differ on day-to-day ergonomics in ways that compound over months of use:
- Apify: Actor configuration is JSON; you submit input via the dashboard, API, or CLI. Logs are structured JSON viewable in the dashboard. Output flows to a Dataset that can be exported to CSV, JSON, or pushed to S3. Schedule via the dashboard or via API. Versioning is git-based for custom Actors.
- Octoparse: Visual workflow editor with drag-and-drop steps. Logs are text only. Output flows to local CSV or cloud storage. Scheduling via the desktop app or cloud dashboard. Versioning is non-existent; saving overwrites the prior version.
- ParseHub: Browser-based visual editor with point-and-click. Logs are basic text. Output flows to JSON/CSV/Excel. Scheduling via the cloud dashboard only. Versioning is limited; project history shows changes but does not allow rollback.
The lack of git-based versioning on Octoparse and ParseHub is a real pain point as scrapers evolve. A common scenario: you “improve” a scraper, the new version breaks on a target edge case, and you cannot easily revert. Apify’s git-based versioning eliminates this entire class of problem.
Apify-specific considerations
Apify’s strength is its Actor marketplace. Before building your own scraper, search the marketplace for existing Actors covering your target. Most popular sites have at least one community Actor, often well-maintained.
Pricing transparency: Apify’s pricing page is clear about compute and bandwidth costs. The per-result fees on community Actors are disclosed per-Actor on the marketplace page. Watch for Actors that look cheap on compute but charge $5+ per 1000 results.
Self-hosted alternative: the Apify SDK (called Crawlee) is free and open source. You get the framework features without the cloud costs. We covered this in our best Node.js scraping libraries 2026 review.
Octoparse-specific considerations
Octoparse’s desktop app is the primary product. The cloud option exists but is more constrained. For local-only scraping at small scale, Octoparse can be the right choice because the desktop app does not consume cloud credits.
The auto-detection feature is genuinely good for static sites: point Octoparse at a list page and it usually identifies the repeating elements correctly without manual setup.
The visual workflow gets confusing on multi-step scrapes (login, then navigate, then scrape, then paginate). For workflows beyond 5-10 steps, the abstraction starts breaking down.
ParseHub-specific considerations
ParseHub’s desktop client is required even for cloud-run scrapers (you build in desktop, run in cloud). The Mac/Windows clients work but the UX is dated.
The pricing model (pages per run) is the most unusual in the space. A “page” is one HTTP request to the target. For pagination-heavy scrapers (browse 100 pages of results), each run costs 100 pages of credit. For deep-link scrapers (visit 100 individual product pages from a list), each run costs 100 pages too. Budget accordingly.
Common gotchas
- Apify community Actor staleness. Some Actors are abandoned and silently fail when the target updates. Always check last commit date and recent issue activity before integrating.
- Apify per-result fees stack with compute. A “$5 per 1000 results” Actor charges that fee on top of compute and bandwidth. Total cost is easy to underestimate by 30-50%.
- Octoparse desktop-only quirks. Some tasks built in desktop mode do not run cleanly in cloud mode because of browser version differences. Test cloud runs before relying on them.
- Octoparse anti-bot ceiling. The built-in proxy options are limited. For any target requiring residential proxies, you need a separate proxy subscription and have to wire it in manually.
- ParseHub page count surprises. A “page” is one HTTP request, including all the assets the browser pulls. JS-heavy pages can count as 5-10 “pages” against your quota. Monitor actual usage closely.
- Apify input schema changes. Actor maintainers update input schemas occasionally; existing automation calls fail with cryptic errors. Subscribe to your critical Actors’ release notes.
- Octoparse and ParseHub cookie handling. Both struggle with multi-step login flows that involve OAuth or 2FA. Prepare for manual cookie injection or skip these targets entirely.
- Apify storage retention defaults. Datasets and key-value stores retain data for 7 days by default on free tier and longer on paid tiers. Long-term data needs explicit export to S3 or your own database.
Build vs buy decision for scraping platforms
The “platform vs DIY” decision splits along these lines:
- Use a platform if you do not have engineering capacity, you scrape known targets that platforms have community scrapers for, or your volume is moderate.
- Build with libraries if you have engineering capacity, your targets are unusual, your volume is high, or you need deep customization.
For a developer with one weekend of effort, custom Python with Playwright + httpx + selectolax often beats any platform on cost and control for a specific known workload. For a non-developer or for breadth across many targets, platforms win.
Cost worked example
For a marketing agency scraping 50,000 product pages from Amazon, Walmart, and a regional retailer monthly:
- Apify (community Actors): Amazon Scraper at ~$15/$1k results = $750. Walmart Scraper similar = $400. Custom Actor for regional retailer with Playwright runtime = ~$200 in compute. Total: ~$1350/month.
- Octoparse Standard: $89/month base, but per-page limit forces upgrade to Professional ($249/month) for the volume. Plus separate proxy subscription ($50/mo). Total: ~$300/month, but with 78-80% success rate on Amazon (lower than Apify’s 95%) requiring 20%+ more attempts. Effective: ~$360/month.
- ParseHub Professional: $599/month. Manual proxy integration. Effective: ~$650/month.
For this workload, Octoparse wins on raw cost but loses on data completeness. Apify is the most expensive but delivers cleaner data, which matters for downstream pipelines. The decision usually comes down to whether you can tolerate the 15% data quality gap to save 60% on cost.
Trial and testing
All three offer free tiers:
- Apify: $5/month free credit, no credit card
- Octoparse: 10 tasks, 10,000 records per task on free tier
- ParseHub: 200 pages per run, 5 projects on free tier
Use the free tier to test on your actual targets. Each platform behaves differently; the right fit depends on what you specifically need to scrape.
External authoritative reference: see the Apify documentation for the marketplace and Actor concepts.
What to skip
Octoparse for high-volume use cases: the per-task and per-page limits make it expensive at scale. Switch to Apify or custom code.
ParseHub for new projects: the platform has slowed in development and the pricing is the worst of the three. Octoparse covers similar use cases at lower cost.
Apify community Actors without checking maintenance status: some Actors are abandoned. Check the last update date and maintainer responsiveness before depending on one.
FAQ
Q: which is the best for a complete beginner?
Octoparse. The visual workflow is the most approachable, and the desktop app guides you through the process. ParseHub is similar but the UX is more dated.Q: can I use Apify without writing code?
Yes, by using community Actors. Configure inputs, click run, get results. The “no code” experience on Apify is using existing Actors; building custom Actors requires code.Q: which scales best?
Apify, by a clear margin. The platform was designed for high-volume cloud scraping. Octoparse and ParseHub hit operational limits past moderate volume.Q: do these platforms handle CAPTCHAs?
Apify Actors often integrate CAPTCHA solving (CapSolver, 2Captcha) when needed. Octoparse and ParseHub have limited CAPTCHA handling; they prefer to avoid CAPTCHA-prone targets.Q: is data privacy/GDPR handled?
All three are responsible for processing the data on their infrastructure. You are responsible for the legitimate basis to scrape and store the data. Apify has the strongest documented data processing terms.Q: which platform has the best customer support?
Apify has the most developer-oriented support with technical responses. Octoparse has friendly chat support oriented toward non-developers. ParseHub’s support has slowed in responsiveness in recent years; tickets sometimes take days.Q: do they support scheduling and recurring runs?
All three do. Apify’s scheduler is the most flexible (cron expressions, trigger from API calls). Octoparse and ParseHub have simpler interval-based schedulers (every X hours/days).Q: can I share scrapers with my team?
Apify has team-account features with role-based access. Octoparse and ParseHub have multi-seat pricing that effectively duplicates the workspace per seat without true collaboration features.Closing
Apify is the right pick for most professional scraping in 2026, even for non-developers, because the community Actor marketplace covers most popular targets. Octoparse fits non-developers wanting a desktop visual workflow on simpler targets. ParseHub overlaps with Octoparse but is less competitive on price. Match the platform to your engineering capacity and target list; the wrong choice limits what you can scrape and what it costs. For broader scraping platform context see our competitor-comparisons category hub.
-
-
ScraperAPI vs ZenRows vs ScrapingBee: 2026 head-to-head
ScraperAPI vs ZenRows vs ScrapingBee: 2026 head-to-head
ScraperAPI vs ZenRows is the most common comparison shoppers make when evaluating mid-tier scraping APIs in 2026, and ScrapingBee belongs in the same conversation. The three services occupy similar price tiers and serve similar use cases (managed proxy + browser + anti-bot for general-purpose scraping), but they have meaningfully different strengths. ScraperAPI is the established incumbent with the most predictable behavior. ZenRows is the modern challenger with the best Cloudflare bypass we measured. ScrapingBee is the indie-friendly option with the cleanest rendering and screenshot capabilities. We ran the same workloads against all three for 60 days; the differences are real and the right pick depends on your specific target mix.
This guide compares the three services head to head on success rate, pricing transparency, anti-bot capability, JavaScript rendering, integration ergonomics, and use case fit.
Quick summary
If you are scraping a mix of general-purpose targets at moderate scale, ScraperAPI gives you the most predictable cost and behavior. If your workload is dominated by Cloudflare-protected sites, ZenRows wins on success rate. If you need rendering, screenshots, or PDF generation alongside scraping, ScrapingBee has the cleanest implementation. For protected targets at scale, ZenRows premium mode beats ScraperAPI’s premium mode in our testing.
The honest truth: all three work for most scraping use cases. The right choice depends on which specific targets you face most often and how much you value cost predictability versus peak success rate.
Pricing comparison
All three use credit-based pricing with multiplier logic for harder requests. The base credit count is meaningful, but the multipliers determine your real cost.
service starter plan credits per-1000-credits price hard target multiplier ScraperAPI $49/mo 100,000 $0.49 5-25x for premium pool ZenRows $69/mo 250,000 $0.28 5-25x for premium proxy ScrapingBee $49/mo 150,000 $0.33 5-75x for premium and JS The credit per dollar varies, but more important is the multiplier behavior on real targets:
target ScraperAPI credits/req ZenRows credits/req ScrapingBee credits/req basic HTML page 1 1 1 JS rendering 5 5 5 Amazon (premium) 25 10 75 Google SERP 25 25 25 LinkedIn 25-50 25 not officially supported Cloudflare-protected (premium) 25 10 (their cheapest premium tier) 25 The multiplier on Amazon is where the real cost differs. ZenRows charges 10 credits per Amazon request on their cheaper premium tier. ScraperAPI charges 25. ScrapingBee charges 75. For an Amazon-heavy workload, ZenRows is dramatically cheaper despite higher headline pricing.
For a workload doing 100k Amazon page requests per month:
service total credits needed tier required monthly cost ScraperAPI 2.5M Pro $149 (1M credits) + extra ~$300 ZenRows 1M Pro $129 (1M credits) $129 ScrapingBee 7.5M Business $399 (3M credits) + extra ~$1000 The pricing reality differs dramatically based on target mix.
Success rate comparison
We measured success rates across six representative targets over 60 days, with each provider’s “premium” mode enabled on the harder targets.
target ScraperAPI ZenRows ScrapingBee Amazon US 88% 91% 87% Walmart 91% 94% 90% Cloudflare-protected SaaS 79% 91% 78% Google SERP (top 10 results) 92% 91% 89% LinkedIn public profiles 75% 78% (not supported well) Booking.com 88% 92% 87% ZenRows wins on Cloudflare-heavy targets by a meaningful margin. ScraperAPI is competitive on standard e-commerce. ScrapingBee trails on protected targets but is competitive on basic targets.
The Cloudflare gap (91% vs 78-79%) is real and reflects ZenRows’ specific investment in Cloudflare bypass. If your target list is Cloudflare-heavy, this gap dominates the comparison.
Decision matrix: solopreneur, SMB, enterprise
profile volume / mix recommended primary secondary reasoning Solopreneur testing <10k req/mo ScraperAPI free tier ZenRows free tier Lowest entry, generous trial Indie scraper, mixed targets 10k-200k req/mo ScraperAPI Pro ZenRows Pro fallback Predictable cost, decent baseline Indie scraper, Cloudflare-heavy 10k-200k req/mo ZenRows Pro ScraperAPI fallback Cloudflare success premium worth it SMB ops, broad target catalog 200k-2M req/mo ZenRows Business ScraperAPI failover Per-request cost wins on hard targets SMB ops, rendering-heavy 100k-1M req/mo ScrapingBee Business ZenRows Best JS interaction support Enterprise data ops 2M+ req/mo Bright Data Web Scraper ZenRows + Oxylabs Specialist enterprise products dominate Single-target dedicated any DIY with httpx + proxy API as failover Custom scraper cheaper on one target The most common mistake is choosing on headline price without modeling the target multiplier. ZenRows at $69/mo looks more expensive than ScraperAPI at $49/mo until you compute Amazon credits at 10 vs 25 multiplier; then the picture flips.
Migration path between the three
The three APIs share enough surface that migration is mostly a parameter rename. The playbook:
- Wrap each API behind a uniform
scrape(url, options)interface. All three return raw HTML; the differences are parameter names and base URLs. - Run parallel for two weeks sending 10-20% of production traffic to the new vendor. Compare success rate, latency, and cost-per-success on YOUR targets.
- Cut over by surface, not by total traffic. The vendors differ per-target; ZenRows may dominate on Amazon while ScraperAPI dominates on a regional retailer.
- Maintain the old subscription for 30 days post-cutover as a safety net.
- Re-evaluate quarterly. Vendor quality shifts with each round of bot-detection updates from major target sites.
Anti-bot capability
All three handle the basic anti-bot stack: rotating residential proxies, browser fingerprinting, header rotation, JavaScript challenge solving. Differences are at the edges:
ScraperAPI: stable but conservative. They handle reCAPTCHA v2/v3 automatically on premium tier. Cloudflare bypass works on most sites but struggles with Cloudflare’s bot fight mode at maximum settings.
ZenRows: best Cloudflare bypass in our testing. Their “Premium Proxy” mode specifically targets Cloudflare’s TLS fingerprinting and behavioral checks. Also handles DataDome and PerimeterX better than the others. CAPTCHA solving included on premium tier.
ScrapingBee: solid CAPTCHA handling, less aggressive on Cloudflare. Their differentiation is the rendering side rather than the anti-bot side.
JavaScript rendering
All three offer JavaScript rendering as a credit-multiplied option. The implementation quality differs.
ScraperAPI: rendering works for most SPAs. Limited customization (no JavaScript injection, no screenshot, no PDF). Wait conditions are basic: wait for selector, wait for time.
ZenRows: rendering is fast. Supports custom JavaScript injection (run arbitrary code on the page after load). Wait for selector. Limited screenshot support.
ScrapingBee: most flexible rendering. Custom JavaScript injection, full screenshot capability (full page or selector), PDF generation, click and type interactions before scraping. The closest thing to a managed Playwright service among the three.
If your scraping requires interaction (clicking buttons, filling forms, multi-step flows), ScrapingBee is the right pick. If you just need to fetch the rendered HTML, ScraperAPI or ZenRows are simpler and cheaper.
Integration ergonomics
All three offer REST API and proxy-style integration. Code examples for each:
ScraperAPI:
import requests resp = requests.get( "https://api.scraperapi.com", params={ "api_key": "YOUR_KEY", "url": "https://target.example.com", "render": "true", "premium": "true", "country_code": "us", }, timeout=60, ) print(resp.text)ZenRows:
import requests resp = requests.get( "https://api.zenrows.com/v1/", params={ "apikey": "YOUR_KEY", "url": "https://target.example.com", "js_render": "true", "premium_proxy": "true", "proxy_country": "us", }, timeout=60, ) print(resp.text)ScrapingBee:
import requests resp = requests.get( "https://app.scrapingbee.com/api/v1/", params={ "api_key": "YOUR_KEY", "url": "https://target.example.com", "render_js": "true", "premium_proxy": "true", "country_code": "us", "screenshot": "true", # ScrapingBee specific }, timeout=60, ) print(resp.text)The three APIs are nearly identical in shape. Migration between them is mostly a parameter rename. None has a dramatic ergonomic advantage.
Comparison table
dimension ScraperAPI ZenRows ScrapingBee starting price $49/mo $69/mo $49/mo credits per dollar average best mid Amazon multiplier 25 10 75 Cloudflare success 79% 91% 78% general success 92% 94% 90% JS rendering quality basic good best CAPTCHA solving included premium included premium included premium screenshot support no limited full PDF generation no no yes custom JS injection no yes yes sticky session yes (10 min) yes (10 min) yes (5 min) best for predictable mid-tier Cloudflare-heavy targets rendering and screenshots Use case to provider mapping
use case best fit general e-commerce scraping at moderate scale ScraperAPI Amazon-focused scraping at scale ZenRows Cloudflare-protected SaaS scraping ZenRows premium sites needing screenshots/PDFs alongside scraping ScrapingBee multi-step interaction (click, type, then scrape) ScrapingBee SERP scraping ZenRows or dedicated SERP API (SerpApi/DataForSEO) LinkedIn public profile scraping ZenRows (but consider Bright Data LinkedIn Scraper instead) budget-constrained generic scraping ScraperAPI lowest tier highly dynamic SPAs with form interaction ScrapingBee Concurrency and throughput differences
Beyond per-request success rate, the three differ on burst tolerance:
- ScraperAPI: Pro tier allows 50 concurrent requests, Business 100, Enterprise custom. The throttling is enforced at the infrastructure level; exceeding the cap returns 429 immediately.
- ZenRows: Pro 25 concurrent, Business 50. Lower default than ScraperAPI but the throttling kicks in more gracefully (queueing rather than 429).
- ScrapingBee: Freelance 5 concurrent, Startup 10, Business 40. The lowest default concurrency of the three, which becomes a bottleneck on bursty workloads.
For a scraper that runs every 5 minutes and needs to fetch 1,000 URLs in 60 seconds, you need 1000 / 60 = 17 requests/sec sustained, which works comfortably with 50 concurrent on ScraperAPI Pro but requires Business tier on ZenRows and ScrapingBee.
If your workload is steady-state low-burst, the headline price tier is enough. If your workload is bursty (cron-driven full-catalog refreshes, event-triggered scrapes), upgrade for the concurrency before the credit count.
Webhook and async patterns
For batch workloads where you submit thousands of URLs and process results asynchronously, two patterns work:
- ScraperAPI’s batch endpoint accepts up to 50 URLs per submission and processes them in parallel. Results return inline as JSON arrays. Simple but caps at 50 URLs per call.
- ZenRows webhooks let you submit URLs with a callback URL; results POST back as they complete. Higher complexity but scales to tens of thousands of URLs per batch.
- ScrapingBee batch mode works similarly to ZenRows webhooks. Their async API is newer and the docs are still maturing.
For sub-100-URL batches, sync mode is simpler. For thousands of URLs, async webhooks are the only sane option; the wait time on a sync 1000-URL batch is too long.
Cost analysis at different scales
For three workload sizes:
10k requests/month, mostly basic HTML:
– ScraperAPI: $49/mo (free tier covers it)
– ZenRows: $69/mo (free tier covers it)
– ScrapingBee: $49/mo (free tier covers it)
– Winner: any, pick on success rate for your targets.100k requests/month, mixed targets including some premium:
– ScraperAPI: $149/mo (Pro tier)
– ZenRows: $129/mo (Pro tier)
– ScrapingBee: $99/mo (Freelance tier) but credits run out fast on premium
– Winner: ScraperAPI for predictability, ZenRows for Cloudflare-heavy mix.1M requests/month, heavy premium target use:
– ScraperAPI: $999/mo (Business)
– ZenRows: $499-999/mo (Business)
– ScrapingBee: $1499/mo (Business+)
– Winner: ZenRows by a clear margin.We cover the broader scraping API market in our best web scraping APIs 2026 review.
Common gotchas
- Credit-multiplier surprises. Targets reclassified as “premium” raise their multiplier silently. Track credit-burn-per-target weekly so you catch reclassifications before the bill arrives.
- Geo flag price tiers. All three charge extra credits for geo-targeted requests beyond default US/EU. ASEAN, MENA, and LATAM geos can 2-3x the per-request cost.
- JavaScript rendering on SPAs that auto-redirect. Some SPAs redirect mid-render and the API returns the redirect target’s HTML, not the original. Always check the final URL in the response.
- Sticky session uniqueness. All three pass session ID via a parameter; a typo or collision routes two workers to the same IP and corrupts both sessions. Generate session IDs from worker_id + timestamp to guarantee uniqueness.
- Response size truncation. ScraperAPI and ScrapingBee truncate responses over 5 MB by default. Pages with embedded base64 images can hit this. Specifically opt in to larger responses on plans that support them.
- Free trial concurrency caps. All three throttle free trials to 1-5 concurrent requests. Burst testing fails on the trial; results understate real production capability. Negotiate higher trial concurrency before benchmarking.
- Webhook flakiness on async APIs. ScraperAPI’s async batch mode delivers via webhooks that occasionally drop. Always have a polling fallback that catches missed deliveries.
- CAPTCHA solving included vs add-on. “Premium” tier on each vendor includes some CAPTCHA solving but not all types. hCaptcha and Turnstile are sometimes additional add-ons. Confirm before assuming.
When to use a different service entirely
If the comparison is closer than 5 points across all three on your specific targets, you may benefit from a different service entirely:
- For SERP only: SerpApi or DataForSEO outperform all three.
- For Amazon at extreme scale: Bright Data Amazon Scraper API is more cost-effective than any of the three.
- For LinkedIn: Bright Data LinkedIn Scraper API is the only one that works reliably.
- For full headless browser control: Browserbase managed Playwright.
We compare these alternatives in Bright Data vs Oxylabs vs Smartproxy: 2026 honest review and Apify vs Octoparse vs ParseHub.
Trial and testing
All three offer free trials:
- ScraperAPI: 5000 credits free, no credit card required
- ZenRows: 1000 credits free, no credit card required
- ScrapingBee: 1000 credits free, no credit card required
Use the trial credits on your actual target URLs, not on httpbin.org. The success rate variation between targets is large; testing on the wrong target gives you the wrong answer.
import requests import time API_CONFIGS = { "scraperapi": { "url": "https://api.scraperapi.com", "params_template": lambda url, key: {"api_key": key, "url": url, "render": "true", "premium": "true"}, }, "zenrows": { "url": "https://api.zenrows.com/v1/", "params_template": lambda url, key: {"apikey": key, "url": url, "js_render": "true", "premium_proxy": "true"}, }, "scrapingbee": { "url": "https://app.scrapingbee.com/api/v1/", "params_template": lambda url, key: {"api_key": key, "url": url, "render_js": "true", "premium_proxy": "true"}, }, } YOUR_TARGETS = [ "https://www.amazon.com/dp/B08N5WRWNW", "https://www.your-actual-target.com", ] KEYS = {"scraperapi": "...", "zenrows": "...", "scrapingbee": "..."} def test(service: str, samples: int = 50): config = API_CONFIGS[service] success = 0 latencies = [] for _ in range(samples): for target in YOUR_TARGETS: start = time.monotonic() try: resp = requests.get( config["url"], params=config["params_template"](target, KEYS[service]), timeout=60, ) latency = (time.monotonic() - start) * 1000 latencies.append(latency) if resp.status_code == 200 and len(resp.text) > 5000: success += 1 except Exception: pass print(f"{service}: success {success}/{samples*len(YOUR_TARGETS)}, median latency {sorted(latencies)[len(latencies)//2]:.0f}ms") for s in API_CONFIGS: test(s)What to skip
ScraperAPI’s lowest tier for hard targets: the basic pool struggles with anti-bot. Pay for premium or pick a different service.
ZenRows for simple HTML scraping: the premium pricing is overkill. Use ScraperAPI or roll your own with httpx.
ScrapingBee for high-volume Amazon: the 75x multiplier on Amazon makes the cost prohibitive at scale.
External authoritative reference: see the ZenRows API documentation for technical details on their parameters and pricing model.
FAQ
Q: which has the best uptime?
All three have 99.9%+ uptime SLAs and meet them in practice. Outages happen rarely; when they do, all three notify users via status page.Q: do they refund failed requests?
ScraperAPI refunds requests that return 4xx/5xx from their service. ZenRows refunds blocked requests automatically. ScrapingBee refunds on a per-request basis with a slightly stricter policy. Read the fine print before committing.Q: which is best for SERP?
None. Use SerpApi or DataForSEO. The general-purpose APIs are more expensive and less accurate for SERP than dedicated alternatives.Q: can I switch between them easily?
Yes. The APIs are similar enough that switching is a parameter rename and a base URL change. Many production setups use one as primary and another as failover.Q: which has the best documentation?
ScrapingBee. Clear examples, well-organized parameter reference, working code samples. ZenRows is a close second. ScraperAPI is functional but less polished.Q: do they support Asian targets well?
ScraperAPI and ZenRows both have Asian residential coverage on premium tiers. ScrapingBee’s Asian coverage is thinner. For Japanese, Korean, or Indonesian targets, do extensive trial testing because results vary.Q: which is best for high-frequency price monitoring?
ZenRows tends to win on per-success cost for repeated polling of e-commerce product pages. ScraperAPI is competitive on basic catalogs.Q: are there any with built-in dataset or marketplace features?
None of the three have a Bright Data Datasets equivalent. For pre-scraped data, look at Bright Data or Apify’s dataset offerings.Closing
ScraperAPI, ZenRows, and ScrapingBee are all production-quality scraping APIs in 2026. ScraperAPI is the safe default for general-purpose mid-tier scraping. ZenRows wins on Cloudflare-heavy and Amazon-heavy workloads. ScrapingBee wins on rendering quality and screenshot/PDF needs. Pick based on your specific target mix; the wrong choice can cost 3-5x more than the right one. For broader scraping API context see our competitor-comparisons category hub.
Related comparison: For Singapore-specific work, compare Smartproxy (now Decodo) against a real Singapore carrier network in our SMP vs Smartproxy comparison.
- Wrap each API behind a uniform