Google 'Unusual Traffic' Error With Proxies: Fix
Our systems have detected unusual traffic through proxies? Find the cause in order (IP rate, consent, query strings, subnet) and detect the sorry page in code.
"Our systems have detected unusual traffic from your computer network" is Google telling you that one exit IP, or the network around it, crossed a line it won't name. Through proxies, this usually hits a rank tracker halfway through a run, and the instinct is to rotate harder. That instinct is how a healthy pool gets burned in an afternoon. This guide shows how to read the page you actually received, check the causes in order of cost, and make your code back off the moment the sorry page appears.
The Exact Page You're Getting
We fetched Google's block page directly on 2026-10-30 to record its current wording and headers. This is what came back, with our own IP swapped for a documentation address:
HTTP/1.1 429 Too Many Requests
Cache-Control: no-store, no-cache, must-revalidate
Content-Type: text/html
Server: scaffolding on HTTPServer2
Our systems have detected unusual traffic from your computer network.
Please try your request again later. Why did this happen?
This page appears when Google automatically detects requests coming from
your computer network which appear to be in violation of the Terms of
Service. The block will expire shortly after those requests stop.
...
Sometimes you may see this page if you are using advanced terms that robots
are known to use, or sending requests very quickly.
IP address: 203.0.113.47
Time: 2026-10-30T13:04:52Z
URL: https://www.google.com/search?q=test
Four details on that page do most of the diagnostic work:
- The status is 429, not 200. Our capture returned HTTP 429 with the HTML body. Some tutorials say the sorry page arrives as a 200; if you see a 200 with this text, you are probably reading a rendered browser page after the redirect, not the raw response.
- The path is
/sorry/index. Blocked search requests land athttps://www.google.com/sorry/index?continue=..., with your original search URL incontinue. - The
IP address:line names the exact IP Google blamed. Through a proxy, this should be the proxy's exit IP. If it shows your server's own address, your proxy isn't being used at all, and no amount of rotation will help. - "The block will expire shortly after those requests stop." Google's own wording. It doesn't give a duration, so don't trust anyone who quotes one. It does tell you the remedy is to stop sending from that IP, not to retry it.
Google's help article on the error adds that the page "most likely shows a reCAPTCHA", and that a network, "including VPN networks", can trigger it when it "seems to be sending automated traffic to Google Search". Our direct fetch showed the "try your request again later" variant without a challenge, so expect both.
Sorry Page, JavaScript Gate or Consent Wall?
Three different Google responses get reported as "the unusual traffic error", and only one of them is. Mixing them up sends you after the wrong fix.
| What you see | Status (our capture, 2026-10-30) | Marker in the response | What it means |
|---|---|---|---|
| Sorry page | 429 | `/sorry/index` URL, text "detected unusual traffic", `IP address:` line | The exit IP or its network is flagged |
| JavaScript gate | 200 | ` | Your client doesn't run JavaScript |
| Consent interstitial | Redirect | Final URL on `consent.google.com` | No cookie-choice state for that region (not measured by us today) |
| Real results | 200 | Your parser finds organic results | Working |
The JavaScript gate is the one most people misread. From a single server IP, we sent 25 back-to-back search requests with a python-requests user agent. All 25 came back HTTP 200 with a page whose entire visible content was "Please click here if you are not redirected within a few seconds", pointing at /httpservice/retry/enablejs. None of them reached a sorry page. A plain HTTP client never got far enough to be rate-limited; it was turned away for not running JavaScript.
So if your scraper "suddenly gets empty pages" with a 200 status, check for the enablejs marker before you touch your proxy setup. A new IP won't change that answer. A real browser or a rendering API will.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Diagnosis Order, Cheapest Check First
Run these in order. Each step takes less time than the next and rules out a whole class of cause.
- Confirm which page it is (table above). Thirty seconds with
curl -i. - Confirm the exit IP. Compare the sorry page's
IP address:line with the IP your proxy should be using. A mismatch means a proxy configuration bug, not a block. - Read the failing URL. The
URL:line on the sorry page shows the exact query. Look for stacked operators or parameters you didn't mean to send. - Check the cookie jar. Is the session carrying cookies between requests, and does a request from an EU exit hit
consent.google.comfirst? - Measure rate per IP. Count queries per exit IP over the last ten minutes, from your own logs.
- Group blocks by subnet. If sorry pages cluster on one
/24even at low rates, the range is the problem.
Here's step 1 and step 2 in one go, through SparkProxy's rotating port:
# What did Google send back, and where did it redirect?
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" \
-x "http://USER:PASS@gateway.sparkproxy.io:11000" \
"https://www.google.com/search?q=rank+tracker&hl=en&gl=us"
# Which IP does Google see? The sorry page prints it.
curl -s -x "http://USER:PASS@gateway.sparkproxy.io:11000" \
"https://www.google.com/sorry/index" | grep -o "IP address: [0-9a-fA-F:.]*"
The second command is a cheap trick: when we requested /sorry/index directly on 2026-10-30 without being blocked, Google still rendered the page, including the IP address: line. That's one observation, not documented behaviour, so treat it as a convenience rather than a contract.
Cause 1: Too Many Queries From One IP
Google's page says it plainly: "sending requests very quickly." The variable that matters is queries per exit IP over time, not your total query count. Ten thousand queries spread across a large pool can be quieter, per IP, than three hundred from one address.
Google publishes no safe rate, and we won't invent one. What you can do is measure your own threshold. Pick a conservative starting interval per IP, log every response with its exit IP, and let the sorry rate tell you where the line is for your query mix. The control loop in Back Off Instead of Burning the Pool does exactly that.
Two mistakes make the per-IP rate far higher than you think:
- Retries on the same IP. A retry loop that reuses a Sticky Session port fires the next attempt from the IP that was just blocked. On SparkProxy, port
11002keeps one IP for a five-minute window (Sticky Sessions), which suits paginating one keyword but is the wrong place to retry a block. Retries should go out through port11000(Random Rotation), which hands each request a different IP. - Pagination bursts. Fetching pages 1 to 5 of a keyword in quick succession from one sticky IP is five fast queries. Spread them, or accept a shallower depth.
Sizing the pool from a target per-IP rate is its own piece of arithmetic, covered in proxies for SERP scraping at scale. This section is about the moment it has already gone wrong.
Cause 3: Query Strings Google Reads as Robotic
The sorry page names this cause in its own words: "advanced terms that robots are known to use". Rank trackers and SEO tools generate exactly those queries. Common offenders:
- Stacked operators like
inurl:withintitle:andsite:on every query in a batch, which no ordinary user sends at volume. - Leftover parameters copied from an old tool, such as
num=100. Google stopped honouring that reliably in September 2025, as explained in our guide to scraping Google search results, so it adds risk and returns nothing extra. - Mismatched locale signals, for example
gl=dewithhl=enfrom a US exit. Keep IP country,glandhlconsistent, the same rule datacenter proxies for SEO rank tracking applies to local rank checks.
The sorry page's URL: line makes this easy to audit. Pull the last fifty blocked URLs from your logs and look for a shared pattern. If most of them carry operators and your plain keyword queries sail through, the query text is the cause and no proxy upgrade will change it.
Cause 4: A Flagged Subnet
Google's help page lists shared networks, shared VPNs and shared internet providers as reasons the message appears for someone who did nothing wrong. It even advises network administrators to test IP space before buying it from an IP broker. The proxy version of this is a range whose reputation was spent before you arrived.
The signal is clustering. If sorry pages concentrate on one /24 while other ranges at the same rate stay clean, the range is the cause. The script below groups blocks by subnet using the IP address: lines you've already logged.
import ipaddress
from collections import Counter
def subnet_of(ip: str) -> str:
addr = ipaddress.ip_address(ip)
prefix = 24 if addr.version == 4 else 48
return str(ipaddress.ip_network(f"{ip}/{prefix}", strict=False))
def block_rate_by_subnet(rows):
"""rows: iterable of (exit_ip, was_blocked) tuples from your logs."""
sent, blocked = Counter(), Counter()
for ip, was_blocked in rows:
net = subnet_of(ip)
sent[net] += 1
if was_blocked:
blocked[net] += 1
return sorted(
((net, blocked[net] / sent[net], sent[net]) for net in sent),
key=lambda r: r[1],
reverse=True,
)
sample = [("203.0.113.7", True), ("203.0.113.9", True), ("198.51.100.4", False)]
for net, rate, n in block_rate_by_subnet(sample):
print(f"{net:20} {rate:6.1%} of {n}")
This is the one cause a proxy provider can genuinely fix. Report the clustered ranges with timestamps and the exact sorry-page lines, and ask for a different range. If every range you try clusters at a modest rate, the pool type is the issue, and testing a different one (for example the Scraping API with proxy_type=premium, which the docs price at 10 credits without JavaScript and 25 with it) is the next experiment.
Detect the Sorry Page in Code
Detection has to run on every response, before parsing, and it has to distinguish the four outcomes. A single "blocked or not" boolean throws away the information that tells you what to do next.
import re
from dataclasses import dataclass
from typing import Optional
SORRY_TEXT = "our systems have detected unusual traffic"
IP_LINE = re.compile(r"IP address:\s*([0-9a-fA-F:.]+)")
@dataclass
class Verdict:
kind: str # "sorry", "consent", "js_gate", "candidate" (or "api_limit" below)
flagged_ip: Optional[str] = None
def classify(status: int, final_url: str, html: str) -> Verdict:
lowered = html.lower()
if "/sorry/" in final_url or status == 429 or SORRY_TEXT in lowered:
match = IP_LINE.search(html)
return Verdict("sorry", match.group(1) if match else None)
if "consent.google." in final_url:
return Verdict("consent")
if "/httpservice/retry/enablejs" in lowered:
return Verdict("js_gate")
# Not a known block page. Your SERP parser still has to find results;
# zero organic results on a common keyword should be logged as suspect.
return Verdict("candidate")
Wire it into the fetch so a redirect to /sorry/index is caught even when your HTTP client follows it silently. With requests, response.url is the final URL after redirects:
import requests
PROXY = "http://USER:PASS@gateway.sparkproxy.io:11000"
def fetch_serp(session: requests.Session, query: str, gl: str = "us", hl: str = "en"):
resp = session.get(
"https://www.google.com/search",
params={"q": query, "gl": gl, "hl": hl},
proxies={"http": PROXY, "https": PROXY},
timeout=30,
)
return classify(resp.status_code, resp.url, resp.text), resp.text
A plain requests client like this one will mostly return js_gate today, as our 25-request test showed. Keep the classifier anyway: you'll reuse it unchanged with a browser or with the Scraping API below, and it's what makes the back-off logic possible.
Back Off Instead of Burning the Pool
The pool-burning pattern looks like this: one IP gets a sorry page, the code retries immediately on a new IP at the same speed, that IP gets blocked too, and the run grinds through hundreds of addresses in minutes. Every one of those IPs now carries a fresh mark.
Two separate controls stop it. A per-IP quarantine respects Google's "expire shortly after those requests stop", and a global pace adjusts the whole job when blocks start appearing anywhere. The second is additive-increase, multiplicative-decrease, the same idea TCP uses for congestion.
import random
import time
class Pacer:
"""Global request pacing that slows sharply on blocks and recovers slowly."""
def __init__(self, interval=6.0, floor=3.0, ceiling=300.0):
self.interval = interval # seconds between requests, across all IPs
self.floor = floor
self.ceiling = ceiling
def wait(self):
time.sleep(self.interval * random.uniform(0.7, 1.3))
def on_success(self):
self.interval = max(self.floor, self.interval - 0.1)
def on_block(self):
self.interval = min(self.ceiling, self.interval * 2)
class Quarantine:
def __init__(self, cooldown=1800):
self.cooldown = cooldown # seconds; our assumption, Google publishes none
self.until = {}
def add(self, ip):
if ip:
self.until[ip] = time.time() + self.cooldown
def is_benched(self, ip):
return self.until.get(ip, 0) > time.time()
The starting numbers are placeholders for you to tune against your own logs, not thresholds we measured. A small driver loop ties the pieces together and stops the job outright if blocks keep coming:
def run(queries, session, max_consecutive_blocks=5):
pacer, bench = Pacer(), Quarantine()
streak = 0
for query in queries:
pacer.wait()
verdict, html = fetch_serp(session, query)
if verdict.kind == "sorry":
bench.add(verdict.flagged_ip)
pacer.on_block()
streak += 1
if streak >= max_consecutive_blocks:
raise RuntimeError("Repeated sorry pages: stop the job and investigate")
continue
streak = 0
if verdict.kind in ("consent", "js_gate"):
print(f"{query}: {verdict.kind}, fix the client, not the IP")
continue
pacer.on_success()
yield query, html
The circuit breaker matters more than the pacing. Five sorry pages in a row means something structural changed (a bad range, a query pattern, a broken config), and the cheapest thing you can do is stop spending IPs until a person looks. With Random Rotation, the gateway picks the next exit IP, so the quarantine table is mainly a record you report to support rather than a list you route around yourself.
Running Google Queries Through the SparkProxy Scraping API
If the JavaScript gate is your real blocker, the Scraping API removes it: render_js defaults to true, so each request runs in headless Chromium and costs 5 credits. country_code pins the exit country for 5 more. Ask for json_response=true to get the envelope with the target's status_code, and decode the body, which is base64, before you look for any marker.
import base64
import requests
API = "https://scrape.sparkproxy.io/api/v1"
KEY = "YOUR_API_KEY"
def serp_via_api(query: str, country: str = "us"):
target = requests.Request(
"GET", "https://www.google.com/search", params={"q": query, "gl": country, "hl": "en"}
).prepare().url
resp = requests.get(
API,
headers={"X-API-Key": KEY},
params={"url": target, "country_code": country, "json_response": "true"},
timeout=120,
)
if resp.status_code == 429:
# The API's own rate or concurrency limit, not Google's sorry page.
return Verdict("api_limit"), ""
resp.raise_for_status()
env = resp.json()
html = base64.b64decode(env["body"]).decode("utf-8", "replace")
status = env.get("status_code") or 0
return classify(status, target, html), html
Two traps to avoid here. First, format=json is content conversion (headings, links, metadata), not the envelope; only json_response=true returns status_code and body. Second, a 429 from the API itself means you've hit your plan's rate or concurrency limit, which the docs pair with a retry_after_seconds value. It is a different problem from Google's 429, and your code should keep them apart, as the function does. The docs also list a 530 for a failed scrape, including an anti-bot block, with credits refunded, which raise_for_status() surfaces as an exception.
We couldn't run this function live on 2026-10-30 because the API endpoint was returning a Cloudflare 521 during our test window, so the snippet is written against the published parameter reference rather than a captured response.
What a Proxy Can and Cannot Fix
Be clear about this before buying more IPs. More addresses solve one cause well and the rest not at all.
| Cause | Does a proxy fix it? | The actual fix |
|---|---|---|
| Too many queries per IP | Yes, by spreading load | Random Rotation, lower per-IP pace, no retries on a sticky IP |
| Flagged subnet | Yes | Different range or pool type, reported with sorry-page evidence |
| Proxy not in use (your own IP on the sorry page) | No | Fix the client's proxy configuration |
| JavaScript gate | No | Real browser or a rendering API |
| Missing consent state | No | Persistent cookie jar, handle `consent.google.` separately |
| Robotic query strings | No | Drop stacked operators and dead parameters |
| Total volume against Google's terms | No | Reduce volume; use Search Console for your own site's data |
For the causes a proxy does fix, SparkProxy plans start from $75/month (USD, read from sparkproxy.io on 2026-10-09) with unlimited bandwidth, so you pay for the plan and its concurrency, not for traffic. The pool is 1M+ premium IPs across 80+ countries over HTTP/HTTPS and SOCKS5, with user:pass or IP whitelist auth. There's a 24-hour free trial with no credit card, which is enough time to run the subnet grouping above against your own keyword set. If CAPTCHAs, rather than sorry pages, are your main issue, how to avoid CAPTCHAs when web scraping covers that side.
Frequently asked questions
FAQ
Google has decided that requests from the proxy's exit IP, or from the network around it, look automated. The page is served at /sorry/index with HTTP 429 in our 2026-10-30 capture, and its IP address: line shows which IP was blamed.
Google doesn't publish a duration. The page itself says "The block will expire shortly after those requests stop", so stop sending from that IP and give it a cooldown you tune from your own logs.
Either the IP's range was already flagged by earlier users, or your queries trip the "advanced terms that robots are known to use" check. Group blocks by subnet and audit the blocked URLs to tell the two apart.
No. In our test, a plain HTTP client received HTTP 200 with a meta refresh to /httpservice/retry/enablejs, which is Google's JavaScript gate. A new IP won't fix it; a browser or a rendering API will.
No. Retrying from the blocked IP extends the block. Send the retry through Random Rotation after a delay, and stop the job entirely if several sorry pages arrive in a row.
They can help when the cause is a flagged range, because the exits sit on consumer ISP networks instead of hosting ranges. They don't help with the JavaScript gate, missing consent state, or robotic query strings.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles
Sticky Session IP Changing Mid-Flow? Causes and Fixes
Sticky session IP changing mid-login or checkout? Check leaks, session ID format, the 5-minute window, parallel connections and exit churn in order.
httpx proxies Argument Removed? Fix ProxyError and 407
The httpx proxies argument is gone in 0.28. Fix the TypeError, then ProxyError 407, SOCKS ImportError and RemoteProtocolError, with verbatim errors and code.
npm config set proxy: Install Packages Behind a Proxy
npm config set proxy and https-proxy, the right .npmrc file, yarn and pnpm equivalents, noproxy for private registries, and keeping passwords out of your repo.
