Picking up from the Decodo baseline test: the shipping engine floors at ~44% on Decodo rotating and every renderer-side patch I tried on top either didn't help or made it worse. This post is the full A/B on the one lever I hadn't personally tested yet: the "humanize your scraper" advice from every tutorial on the internet.
I've been scraping SERPs on and off, since 2019. Honestly it was so easy to scrape Google or Bing or any other search engine till 2025. Especially in 2025 with its 10 results only per page, and with its new /goto redirect in 2026, Google is really making it very difficult to do small scale SEO/AEO studies around SERPs. And these are just business level decisions we see out clearly.
When you dig deep in scraping serps on G, you can clearly see the effort they are putting in to making sure its human on the other end. The browser fingerprints, the cookie-ip matching, the need for JS execution are all making it either hard or cost inefficient.
And no wonder there are ton of humanize your scrape for serp scraping tutorials out there. The random delays, scroll gestures, fake mouse movements, 3-7 second dwell times are some simple yet popular suggestions.
I am trying to build an open SERP checker for the SEO community, and I needed to know which pieces of the standard "stealth" playbook are actually load-bearing versus pure theatre. In my efforts to get cost efficient success with G-SERP scraping, I started exploring these humanize tutorials both on home system and on residential proxy providers.
From the other proxy test I did to check which Ip pools are good for SERP scraping, Decodo residential ip pool came out to be the most resilient. So for proxy layer, I used Decodo.
I built a humanize the scrape script and used it to scrape 10 real Google queries. And, here are the results:
1. TL;DR
- Humanize gestures don't fix a burned IP.
- Humanize gestures do not fix a substandard scraping script.
- On Google.com/search a scraping script that does not give away the bot sign and not-burned residential proxies (IP reputation) dominates everything.
Scroll + dwell + mouse-move = cost, not a real browsing session signal.
Indeed it increased bandwith usage by 68% and latency by 3-6 times.
So what is humanize your scrape better for? Scraping Cloudflare-fronted apps or logged-in flows: humanize still matters there.
Humanization made my residential proxy bill 68% higher, my latency metrics got 3-6× worse, and my pass rate zero percent better. That's the article in one sentence and the rest is the receipts. But those receipts give you insights on how Ip pools are playing a role.
2. The setup
Like I mentioned, I have been working on open serp scraper and for that, before this test I'd already tried:
- Plain
curl(works but returns the JS-less SERP). curl_cffiwith Chrome TLS impersonation (hits/sorry/after 2-3 queries), Playwright Chromium (silent TLS hang 0/3 on Decodo residential).- Playwright Firefox (TLS 3/3 but 10/10
/sorry/at/search). - Playwright stealth patches (
navigator.webdriverclean, still 10/10 on first try and later gets/sorry/). So, not so reliable.
Every rung confirmed the same thing: once Google's IP-reputation layer flags the exit, nothing above layer 1 saves you.
The point I am making is I already have a decent script ready.
Humanize was the one piece of the standard playbook I hadn't personally tested. It felt like obvious next move after fingerprint patches. So I added it as part of the script.
3. The methodology
Same 10 keywords, same current Chromium engine (fresh browser per fetch, blocking images/media/fonts), same Decodo rotating pool on port :10000. Interleaved A/B: alternating variants so pool-state drift affects both symmetrically.
A straight "10 A then 10 B" run would confound "which variant worked" with "which IPs happened to be fresh in that half of the session," and Decodo's rotating pool refreshes faster than Google's reputation feed updates.
- A (baseline): warmup → search → wait for h3 → close.
- B (humanized): warmup → search → wait for h3 → random 3-7s dwell + scroll (
mouse.wheeltwice) + 1-2 fake mouse moves → close.
Both variants used random 15-45s inter-query pacing. I pulled the pacing variable out of the comparison on purpose, because every tutorial already agrees that you shouldn't burst Google.
4. The results
This is raw data:
[A q1] 'foxy ai promo code' ip=41.193.83.231 h3=10 bytes= 541KB lat= 6.8s → OK
[B q2] 'heygen promo code' ip=77.250.212.248 h3= 9 bytes= 584KB lat= 9.7s → OK
[A q3] 'apify promo code' ip=46.53.149.178 h3=10 bytes=1193KB lat= 8.2s → OK
[B q4] 'webshare coupon code' ip=92.98.255.72 h3=10 bytes=2308KB lat=12.2s → OK
[A q5] 'gamestop promo code' ip=86.122.176.34 h3=10 bytes= 650KB lat= 4.6s → OK
[B q6] 'midjourney promo code' ip=87.208.252.204 h3= 0 bytes=1689KB lat=18.9s → SORRY
[A q7] 'twilio promo code' ip=105.157.123.240 h3= 0 bytes=1828KB lat= 9.8s → SORRY
[B q8] 'bnesim coupon' ip=81.65.150.50 h3= 0 bytes=1637KB lat=31.2s → SORRY
[A q9] 'retroid discount code' ip=41.90.184.118 h3=10 bytes= 622KB lat= 7.4s → OK
[B q10] 'paymore discount code' ip=41.23.42.75 h3=10 bytes=1916KB lat=18.7s → OK
| Variant | Pass rate | Avg bytes | Latency |
|---|---|---|---|
| A (baseline) | 4/5 (80%) | 967 KB | 4-10 s |
| B (humanized) | 3/5 (60%) | 1627 KB | 9-31 s |
| Delta | -1 | +68% | +3-6× |
The headline: at N=5 per variation with and without humanizing, a single-pass delta is deep inside noise, so pass rate shows no signal.
Every query hit a unique exit IP: Decodo's rotating pool was doing its job.
The /sorry/ clusters (twilio in A, midjourney and bnesim in B) landed on Netherlands, France, and Morocco IPs; other Netherlands and France IPs passed in the same run. That's IP-lottery variance, not a behavioral signal.
It is impossible for the humanize advice to be helping here, because the thing deciding pass/fail happens before any of those gestures ever get a chance to run.
But what's interesting is the two numbers that are clean, and both go the wrong direction:
-
Bandwidth up 68%. Humanize increased per-query bandwidth from 967 KB to 1627 KB. Scrolling triggers Google's lazy-load: related searches expand, more results paginate in, additional widgets fetch. "Blocking images and fonts" only stops requests the browser tries to make on its own; when I explicitly tell it to scroll, I'm actively asking Google for more content. The scraper tutorials sell humanize as "look like a real user." Nobody mentions that real users also cost more to serve, and lazy-loaded content means lazy-loaded bandwidth bills.
-
Latency 3-6× worse. Baseline was 4-10s per query. Humanized was 9-31s. At 1000 queries/day (a plausible load for a medium-sized SEO agency) this adds 1-4 hours of wall time per daily batch. On a $0.001-per-GB residential plan the bandwidth hit is small money; the latency hit is the one that makes the job feel like a hostage situation.
The honest disclaimer: this test tops out at N=5 per arm. If someone wanted to argue "run N=100 and the pass rate might diverge," that is a fair statistical point. But the mechanism argument below is strong enough that I'm not going to spend another weekend proving it twice.
And, here are my reasons:
5. Why it couldn't have worked
The starting intuition, mine, and every github humanize scripts and web tutorials is: our lightweight fetches look like scripts; make them heavier, make them look human, pass rate goes up.
The reality: "heavier" isn't the same as "more human." What makes a request look human to Google is not the kb-volume of what you load, it's the identity of the source IP and its history of consistent behavior. A residential proxy exit IP with no logged-in Google session, loading 1.6 MB of scrolled SERP content, is not more human than the same IP loading 500 KB of static SERP content. Both are, from Google's perspective, "unknown IP with no session cookie hitting /search."
Google's /search gate scores roughly in this decreasing order of impact:
- Is this source IP on our proxy-intel feed? (yes → captcha)
- Does this IP have a warm user profile or a logged-in Google account? (no → suspicion)
- Is the request cadence from this IP consistent with normal browsing? (noisy; hard to fail unless you're clearly bursting)
- Does the browser fingerprint match a known-clean profile? (easy to pass with stealth Chromium or vanilla Firefox)
- Do JS-runtime signals suggest automation? (
navigator.webdriverand friends) - Does on-page interaction look real? (scroll, dwell, mouse: theatre-tier signal)
Note: Another thing that I am planning to test next weekend is impact of cookies and ip-cookie combinations. Probably this will be like layer 1 or layer 2.
Every humanize tactic you read about on scraper blogs lives at layer 6. The classifier makes most of its decision at layer 1. We're sprinkling flour on the roof while the basement floods.
There's a second, meaner reason the mouse-move half of humanize can't work: in Playwright, page.mouse.move(x, y) dispatches a mouse event with event.isTrusted === false. Any detector that reads this property, which I think modern bot-detection JS absolutely reads it, can tell the event was synthetic. isTrusted is a browser-enforced flag; only genuine hardware-originated gestures get true. There is no Playwright patch for this because the browser itself refuses to lie about it. So the "fake mouse movement" trick that every tutorial recommends is functionally invisible to hostile detectors. Scroll via page.mouse.wheel() does produce real DOM events, but as the bandwidth numbers above show, the cost of being measured outweighs the benefit on a pool where the IP already failed layer 1.
Scroll on other hand probably earns you a slightly better behavioral signature on an IP that already failed layer 1. Dead-on-arrival signal.
For SEOs who've been in the game long enough to remember when Googlebot ignored JavaScript entirely, this will track with your instincts: Google has always weighted who is asking far more heavily than how the asking looks. For newer SEOs cutting their teeth on scraping, the fast lesson is: spend your effort on IP quality, along with scraping script reliability.
6. Where humanize does earn its keep
I don't want this to read as "humanization is useless." It isn't. It's useless on Google /search from a residential proxy pool that's already on the proxy-intel feeds.
On adjacent targets:
- Cloudflare-fronted apps with client-side JS challenges often score behavior across the session (dwell, scroll depth, keystroke rhythm). Humanize helps there.
- Logged-in flows (think scraping behind an authenticated session on a reviews site) weight behavior heavily because the IP+session+cookie triple is already partially trusted.
- Sites without an active IP-reputation feed: mid-tier e-commerce, niche review sites, most of the public web outside the top 500, have no layer 1 to speak of, so layer 6 becomes meaningful.
My coupon scraper, which hits 15+ merchants per store per day across WordPress, Cloudflare-static, and custom FastAPI targets, uses modest humanization on the Cloudflare-fronted ones (a 1-2 second dwell is enough) and strips it entirely on the plain-nginx targets to save latency. That tiered approach only became obvious after running tests like this one and realizing humanize is a tool, not a doctrine.
For SEOs scraping beyond Google, something like competitor Amazon listings, Walmart category pages, Yelp reviews, keep humanize in your kit. For SEOs scraping Google SERPs through residential proxies, move humanize off your list and move "better IPs" onto it.
7. What this means for openserpchecker
Here's the reason I ran this test in the first place: I'm trying to build an openserpchecker for the SEO community. Open as in open-source, open as in "no mandatory paid API in the dependency graph."
Right now the two realistic paths for an SEO consultant who wants programmatic SERPs are Serper.dev at ~$50 for 6 months minimum or DataForSEO at $0.002 per query. For an agency tracking 5,000 keywords twice a week that's $120/month in query fees alone. This is not catastrophic, but it's a tax on being curious. The moment you want to test a weird hypothesis like "what does the SERP look like for 400 variations of this long-tail?" you're thinking about the invoice instead of the data.
The openserpchecker is exactly serving those SEO/AEO professionals who wants to run 500-5,000 queries today, get real organic results, and not pay a middleman for the privilege. For that user, a $5/GB residential sub plus clean code is a better fit than a $50/mo API contract, if the stack actually works. Testing whether "humanize" is one of the levers that makes it work was the first question I needed to answer.
The practical decisions, now that I have the answer:
- Not shipping humanization as a fetch-time feature. No pass-rate improvement + 68% more bandwidth + 3-6× latency means every one of those axes gets strictly worse at Decodo's current IP-reputation ceiling. Shipping it would be shipping a feature that costs users money for zero benefit.
- Investing the saved weekend budget in IP strategy. The current engine (Chromium, blocking images/media/fonts, Decodo
:10000rotating) sits at a 4/9 live pass rate. This is an honest number, published, not vibes. The upside path is residential pool quality, warm sticky sessions with a landing-page warmup, and probably testing mobile IPs for a subset of queries and measuring cookie-ip combination. That's where the next weekend goes.
If you're an SEO who wants the open scraper code the moment it's ready, or if you have strong opinions on which residential pool survives Google best right now, hit me up. That's half the point of writing this: I want the people with the next data point in the thread.
8. Appendix, the full pre-humanize ladder
For completeness, the five rungs I burned through before the humanize A/B. I keep this here instead of in the body because it's a prequel: useful if you're mapping your own escalation path, skippable if you came for the humanize data.
1. Plain curl through the proxy. Baseline sanity check. curl -x gate.decodo.com:10000 https://www.google.com/search?q=... returns HTTP 200 in about 1 second. Parse the HTML and you get Google's "I don't think you have JavaScript" lightweight SERP: the layout that comes back when the request doesn't execute JS. The organic blocks are mostly there, enough to prove the TCP connection works, but you'll miss featured snippets, rich results, and the organic ordering is slightly different from the JS-rendered page. Verdict: fine for health checks, useless as a product fetcher.
2. curl_cffi with Chrome impersonation. The "clever cheat" phase. curl_cffi wraps curl with real Chrome TLS fingerprints: impersonate="chrome120" and your TCP + TLS handshake is byte-for-byte Chrome's. No browser overhead, no 100MB Chromium binary. Worked for maybe 2-3 queries per sticky session before Decodo's residential pool hit a /sorry/ captcha. The catch: Chrome's TLS shape without a Chrome session history on the IP is a tell, and once Google's IP-reputation feed marks that exit IP, no amount of fingerprint accuracy saves you. I was briefly fooled into thinking this was the answer. It isn't, not for sustained scraping.
3. Playwright Chromium (headless). My coupon-scraping system, the one feeding 15+ affiliate sites, each getting one scrape per day, runs on Playwright Chromium, so this was the obvious next step. It silently hung. Not timed-out, not errored, hung. The exact same proxy that returned curl's 200 in 1 second refused to complete Chromium's TLS handshake. Session 2.7.5 of my internal notes confirms Chromium 0/3 against google.com through Decodo :10000. Something at Google's edge was dropping Chromium-shaped ClientHellos coming from residential pools.
4. Playwright Firefox. Firefox has a completely different TLS shape: different cipher order, different extensions, different JA4. Through the identical Decodo pool, on the identical day, Firefox got TLS 3/3 on the handshake. The hypothesis "it's a fingerprint problem" survived first contact. Then I asked Firefox to actually fetch /search, and I got 10 out of 10 /sorry/ captchas on the sticky :10001 port. So the fingerprint fixed layer 1 (TLS) and ran straight into layer 2 (IP reputation at the application layer). Two blocks, not one.
5. Playwright stealth patches. navigator.webdriver = false, patched window.chrome, patched plugins array, patched languages. Self-check green: the stealth plugin correctly reports navigator.webdriver === false. Still 10/10 /sorry/ on the same sticky Firefox session. The data point that mattered: when fingerprint is clean and you're still getting captchas, the signal Google is scoring on is the IP, not the browser. That is the finding that made the humanize test the obvious next experiment.
For SEO practitioners who scrape outside Google (Amazon, Walmart, review sites), the first four rungs still rank-order correctly: curl → curl_cffi → Playwright Chromium → Playwright Firefox is the sensible escalation, and most protection vendors cave somewhere in that ladder. For Google specifically, the ladder ends before you win.
Updates log
- Initial publish. Interleaved A/B on Decodo rotating, mechanism explanation, and the practical decision not to ship humanize as a fetch-time feature.
- Light edits. Added cross-links to the baseline and the parked-tab writeup.