🎉 Premium Proxies · 3-Day Free TrialClaim Now →
Guides

How to Scrape Google Search Results Without Blocks

Scrape Google search results at scale without getting blocked. Learn request pacing, geo-targeting, SERP parsing, and a working SparkProxy API example.

S SparkProxy 2 15 min read
Share
How to Scrape Google Search Results Without Blocks

If you want to scrape Google search results at any real volume, the hard part isn't parsing the page, it's staying unblocked long enough to collect it. Google watches request cadence, IP reputation, and query patterns, and it answers suspicious traffic with a "429", an "unusual traffic" interstitial, or a reCAPTCHA. This guide is the hands-on version: how to pace requests, geo-target them, fetch the SERP through an API, and parse the result types, with working SparkProxy code you can run today. For the theory behind why proxies alone fall short, see the companion explainer on SERP scraping proxies.

Why Google blocks scrapers

Google doesn't block you for scraping one page. It blocks you for looking like automation across many pages. Three signals do most of the work.

Rate and cadence. A single IP firing 60 identical queries a minute at fixed intervals is the clearest bot tell there is. Humans pause, retype, and vary their timing. Uniform cadence gets flagged faster than a high raw count.

IP reputation. Requests from cloud ASNs (AWS, GCP, Azure) start with a worse reputation than residential ISP addresses. Datacenter IPs without careful header work hit "unusual traffic" screens quickly, while residential and mobile IPs last longer because they look like real subscribers.

Request fingerprint. The TLS handshake (JA3/JA4), HTTP/2 header order, and the Sec-CH-UA client hints all have to match a real Chrome build. A bare requests.get() sends a fingerprint no browser produces, so the IP barely matters once that mismatch is visible.

When enough signals stack up, Google returns one of three things: HTTP "429 Too Many Requests", a "Our systems have detected unusual traffic from your computer network" page (HTTP "200" with no results), or a reCAPTCHA challenge. All three mean the same thing: slow down and look more human. The full detection surface and how to counter each signal is covered in how to avoid getting your proxy blocked.

Scrape the public SERP, ethically

Scraping the public search results page sits in contested territory, so set your own guardrails before you write a line of code.

  • Only collect public SERP data. Query results as an anonymous user sees them. Don't scrape logged-in, personalized, or paywalled surfaces.
  • Respect rate limits and back off. If Google returns a "429" with a Retry-After header, honor it. Ignoring back-off signals is both rude and self-defeating; it accelerates the block.
  • Know the Terms of Service. Google's ToS restrict automated access outside its own interface. Scraping public data is not a criminal act in most jurisdictions, but it can breach the contract, so weigh the risk for your use case.
  • Prefer the official API when it fits. Google's Programmable Search Engine JSON API gives 100 free queries a day, then $5 per 1,000 up to 10,000/day. It's a real google search results api, just narrower and pricier at scale than scraping the live SERP. For your own site's ranking data, Search Console API carries zero ToS risk.

If your goal is rank tracking or market research, keep volume proportional to need and cache aggressively. Two teams pulling the same 5,000 keywords hourly is wasteful; most rank data only needs a daily or weekly refresh.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Build the Google search URL correctly

The SERP is just a URL with query parameters. Get these right and you control language, region, device, and pagination without any hidden state.

ParameterPurposeExample
`q`The search query (URL-encoded)`q=running+shoes`
`hl`Interface language`hl=en`
`gl`Country of the results (ISO alpha-2)`gl=us`
`num`Results per page (see the caveat below)`num=10`
`start`Pagination offset`start=10` (page 2)
`uule`Precise location encodingcanonical-name encoded
`tbm`Vertical: `isch` images, `nws` news, `shop``tbm=nws`
`lr`Restrict to a language`lr=lang_de`

A complete URL for US English results looks like this:

https://www.google.com/search?q=running+shoes&hl=en&gl=us&num=10

The num=100 caveat (this is where old tutorials break). For years, scrapers set num=100 to pull 100 organic results in one request and cut request count by 10x. In September 2025 Google stopped honoring num=100 reliably; it now caps most responses near 10 results regardless of the parameter. If a guide tells you to grab 100 results per page, it predates that change. Plan for 10 results per request and paginate with start (0, 10, 20, ...) instead. Budget your credits and pacing around that reality, because it roughly 10x'd the request count for deep-SERP collection overnight.

Pace requests so you look human

Cadence is the single biggest lever you control. The goal is variance, not just delay.

import random
import time

def human_delay(base=25, jitter=0.5):
    """Sleep base seconds ± 50%, drawn uniformly. Never a fixed interval."""
    low = base * (1 - jitter)
    high = base * (1 + jitter)
    time.sleep(random.uniform(low, high))

A fixed time.sleep(25) between queries is itself a fingerprint: real users never hit the millisecond-perfect interval. The random.uniform version above spreads each gap between roughly 12 and 38 seconds, which reads far more like a person refining searches.

Two more pacing rules that matter in production:

  • Cap queries per IP. For residential IPs, keep it near 100 to 200 queries per IP per day spread across hours. Above that, CAPTCHA rates climb steeply.
  • Randomize order and refinements. Don't send 500 keywords in perfect alphabetical order at a steady beat. Shuffle the queue and vary start depth so no two sessions look identical.

If you're managing your own proxy pool, you own all of this. If you use a scraping API, you offload IP rotation and cadence-per-IP to the provider and only control your own concurrency and retry logic, which is most of why teams switch. That trade is broken down in web scraping API vs self-managed proxies.

Geo-target for accurate local results

Google localizes results heavily. A query for "plumber" or "best bank" returns completely different SERPs in London versus New York. If your data has to reflect a specific market, the request has to originate from that market, and the URL has to declare it.

Two layers work together:

  1. Exit IP location. Route the request through a proxy in the target country so Google sees local traffic. Without this, gl alone gives you an approximation, not the real local SERP.
  2. URL geo parameters. Set gl to the country and, for city-level precision, encode a uule location string. Keep hl consistent with the market's language.

Mismatched layers produce garbage: a UK exit IP sending gl=us returns a blend that matches neither market. Keep the IP country, gl, and hl aligned. This is exactly the setup rank trackers rely on, covered in datacenter proxies for SEO rank tracking.

Fetch the SERP with the SparkProxy Scraping API

Instead of assembling proxies, headers, TLS fingerprints, and a headless browser yourself, you can send one request to the SparkProxy Scraping API. It rotates the proxy, renders the page, and returns the SERP HTML. You pass the Google search URL as the url parameter and authenticate with the X-API-Key header.

A minimal cURL request for a geo-targeted, residential-routed SERP:

curl -G "https://scrape.sparkproxy.io/api/v1" \
  -H "X-API-Key: YOUR_API_KEY" \
  --data-urlencode "url=https://www.google.com/search?q=running+shoes&hl=en&gl=us&num=10" \
  --data-urlencode "render_js=true" \
  --data-urlencode "country_code=us" \
  --data-urlencode "premium_proxy=true"

The same call in Python:

import requests
import urllib.parse

query = "running shoes"
serp_url = "https://www.google.com/search?" + urllib.parse.urlencode({
    "q": query,
    "hl": "en",
    "gl": "us",
    "num": 10,
})

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "url": serp_url,
        "render_js": "true",       # run a real browser so SERP features hydrate
        "country_code": "us",      # exit through a US IP
        "premium_proxy": "true",   # residential pool, harder to block
    },
    timeout=90,
)
resp.raise_for_status()
html = resp.text

Each parameter maps to one of the blocking signals from earlier. render_js=true runs Chromium so JavaScript-driven SERP features load and the request carries a real browser fingerprint. country_code=us fixes the exit location. premium_proxy=true routes through residential IPs that survive Google's reputation checks. On the toughest days, add stealth=true, which layers a homepage pre-warm, a forced Google referrer, and longer idle delays on top.

Watch the credit cost, because these stack: premium_proxy with render_js is 25 credits, country_code adds 5, and stealth adds another 5, so a fully loaded SERP request runs about 35 credits. Turn off what a given target doesn't need. Many keyword-only queries succeed without stealth.

For deep SERPs, loop over start with pacing between requests:

def scrape_serp_pages(query, pages=3, gl="us", hl="en"):
    results_html = []
    for page in range(pages):
        serp_url = "https://www.google.com/search?" + urllib.parse.urlencode({
            "q": query, "hl": hl, "gl": gl, "num": 10, "start": page * 10,
        })
        r = requests.get(
            "https://scrape.sparkproxy.io/api/v1",
            headers={"X-API-Key": "YOUR_API_KEY"},
            params={
                "url": serp_url,
                "render_js": "true",
                "country_code": gl,
                "premium_proxy": "true",
            },
            timeout=90,
        )
        if r.status_code == 200:
            results_html.append(r.text)
        human_delay(base=25)   # from the pacing section
    return results_html

Parse the SERP result types

A modern Google SERP is not a list of ten blue links. It's a mixed layout of organic results, ads, and features, and each needs its own extraction logic.

The honest caveat first: Google's CSS class names are obfuscated and rotate often (.g, .tF2Cxc, .yuRUbf and friends change without notice). Don't hard-code brittle class chains. Prefer structural anchors (the result container that holds an

SERP field reference table

Here's what a Google SERP can contain and how to recognize each block. Not every query returns every type; the mix depends on intent (informational, commercial, local).

Result typeWhat it isHow to detect itNotes
Organic resultStandard "blue link" listing`

Handle CAPTCHAs, 429s, and empty pages

Even a well-behaved scraper hits blocks. Detect them explicitly instead of trusting the status code alone, because Google often returns "200" on a block page.

def is_blocked(status_code, html):
    if status_code == 429:
        return True
    signals = ["unusual traffic", "detected unusual", "g-recaptcha", "/sorry/"]
    lowered = html.lower()
    return any(s in lowered for s in signals)

When is_blocked is true, do three things in order:

  1. Back off. Honor any Retry-After, then increase your base delay. If more than 10% of requests over a rolling window are blocked, your rate is too high for the current pool.
  2. Rotate. Get a fresh IP for the retry. With the SparkProxy API this is automatic on the next request; with your own pool you discard the burned IP.
  3. Cap retries. Two or three attempts per query, then log it and move on. Hammering a blocked query wastes credits and deepens the block.

An empty results page with a "200" and no organic containers usually means a soft block or a num/parameter issue, not a genuinely empty SERP. Treat "zero organic results on a common query" as a block signal, not real data.

Scale it: pacing math and batching

Put the numbers together before you launch a big job. Say you track 5,000 keywords daily and need the top 30 results for each. After the num=100 change, top 30 means three requests per keyword (start 0, 10, 20), so 15,000 SERP requests a day.

Do the arithmetic that keeps you unblocked:

  • Concurrency, not just delay. Serial requests with a 25-second gap would take days. Run a bounded pool (for example 10 to 20 concurrent workers through the API) and let the provider spread them across many IPs. You control worker count; the API controls per-IP cadence.
  • Batch by market. Group keywords by gl/country_code so a batch of IPs in one country isn't context-switching regions mid-run.
  • Refresh only as often as the data changes. Most rankings don't move hour to hour. A daily pull for 5,000 keywords is defensible; an hourly pull usually isn't, and it multiplies your block exposure for no extra signal.
  • Cache and dedupe. If two dashboards need the same keyword in the same market, fetch it once and share the result.

This same discipline (pace, geo-batch, cache) is what separates a scraper that runs quietly for months from one that gets throttled in an afternoon. The market-research angle, including how to turn SERP data into competitive insight, is in using proxies for market research and data collection.

Frequently asked questions

FAQ

Scraping publicly visible search results is generally not a criminal act in most jurisdictions, and the hiQ v. LinkedIn ruling narrowed the US Computer Fraud and Abuse Act as applied to public data. It can still breach Google's Terms of Service, which restrict automated access outside Google's own interface, so it's a contractual risk rather than a criminal one. Collect only public data, respect rate limits, and check the rules for your jurisdiction and use case.

Because IP rotation is only one signal. Google also fingerprints your TLS handshake and HTTP header order, and it flags uniform request timing. A residential IP sending 60 identical queries a minute from a non-browser client gets blocked faster than a slower, browser-shaped request. Fix cadence and fingerprint, not just the IP.

Yes. The Programmable Search Engine JSON API returns structured results with 100 free queries a day, then $5 per 1,000 up to 10,000/day. It's cleaner than scraping but narrower and more expensive at scale, and it doesn't return the full live SERP layout. For your own site's rankings, the Search Console API carries no Terms of Service risk.

There's no published limit, and it depends on IP type. As a conservative starting point, keep residential IPs near 100 to 200 queries per day each with randomized timing, which works out to well under a query per minute per IP. To go faster, spread the load across many IPs (a proxy pool or a scraping API) rather than pushing one address harder.

For basic organic results, often no; they're in the initial HTML. For features that load or expand client-side, such as People Also Ask and some carousels, yes. Rendering also produces a real browser fingerprint, which lowers block rates. With the SparkProxy API you enable it with render_js=true.

Google stopped reliably honoring the num=100 parameter around September 2025, so requests now cap near 10 organic results regardless of what you pass. To collect a deep SERP you have to paginate with the start offset (0, 10, 20, ...), which increased request counts roughly 10x for anyone pulling the top 100. Plan credits and pacing around 10 results per request.

Limited-time · 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon — claim it before it's gone

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. SparkProxy provides datacenter and residential proxies plus a managed Scraping API used for SERP collection, rank tracking, and market research at scale. We work with the exact failure modes described here every day, from "429" storms to the September 2025 num=100 change, so the advice reflects what actually keeps SERP jobs running rather than what looks clean in a demo. Our aim is a practical, honest playbook you can adapt to your own targets and volume.

Keep reading

Related articles

How to Use Proxies with Puppeteer

How to Use Proxies with Puppeteer

Use proxies with Puppeteer: set --proxy-server launch args, per-context proxies, page.authenticate for auth, rotation, stealth tips, and error fixes.

SparkProxy·Guides
How to Rotate Proxies in Node.js

How to Rotate Proxies in Node.js

Rotate proxies in Node.js with round-robin, random, weighted, and sticky strategies, plus working axios, got, and node-fetch code and a reusable pool class.

SparkProxy·Guides