Skip to content
CrawlerData
Menu
Guides

Run thousands of Google searches in Python: workers, retries and queues

Measured limits: live Google searches ran at about 0.36 per second with 3, 6 or 10 workers. Batch and retry code for the live and fast queues.

CrawlerData teamPublished 6 min read

Contents

At some point a script that runs one search at a time stops being enough. You have 2,000 keywords to check, or a nightly job that has to finish before morning, and the question is how to run them safely: how many in parallel, what to retry, and when to switch from live requests to a queue.

We measured it. This guide has the numbers from tests we ran on 16 October 2026 against the Google Search API, and tested Python for a batch runner with retries and for the fast queue. The throughput figures come from one account on one afternoon, so use them as a starting point and measure your own.

What we measured

Autocomplete scales with workers. We ran 40 autocomplete requests with 1, 3, 6 and 10 parallel workers, with no retries, so every outcome is raw:

Workers Time for 40 requests Requests per second Median time per request Errors
1 76.3 s 0.52 1.5 s 0
3 24.6 s 1.63 1.4 s 0
6 13.7 s 2.91 1.4 s 0
10 7.0 s 5.75 1.4 s 0

Each step up in workers raised throughput almost in proportion, and there were no 429s or failures up to 10 workers.

Searches do not. We ran 30 real google-search requests, live queue, at three worker counts:

Workers Time for 30 searches Searches per second Median time per search 90th percentile Slowest Errors
3 85 s 0.35 8.1 s 10.8 s 13.2 s 0
6 82 s 0.36 15.2 s 23.8 s 31.2 s 0
10 81 s 0.37 22.2 s 30.1 s 32.1 s 0

The batch finished in about the same time every time. Doubling the workers doubled the time each search took and left the total unchanged. We did not look into why. Whatever the cause, extra workers added waiting and no speed.

For planning, that gives a rough rule: about 0.36 searches per second, so roughly 46 minutes for 1,000 searches and about 7.7 hours for 10,000. We measured one account on one afternoon. Your number may differ, and it can change, so run 30 searches first and use what you get.

The fast queue is for connections, not speed

The live queue holds an HTTP connection open until the search finishes. The fast queue accepts the job straight away, with a 202, and you poll for the result. We submitted 10 searches to it:

  • All 10 were accepted in 3.1 seconds.
  • Polling every 3 seconds took 38 poll requests in total, and every one returned 200.
  • All 10 completed, between 6.1 and 24.0 seconds after we started polling, which is about 0.4 searches per second.

So the fast queue finished at about the same rate as live requests, and it cost the same 1 credit each. Its advantage is practical: you are not holding thousands of connections open, you can submit a whole batch in seconds, and your script can stop and resume.

A batch runner with retries

Put your key in an environment variable:

export CRAWLERDATA_API_KEY="YOUR_API_KEY"
import os
import time
from concurrent.futures import ThreadPoolExecutor

import requests

API_URL = "https://api.crawlerdata.com/v1/jobs"
HEADERS = {"Authorization": f"Bearer {os.environ['CRAWLERDATA_API_KEY']}"}


def search(query, country="us", retries=4):
    """One live search. Retries on 429 and on transient failures whose message says to retry."""
    body = {"endpoint": "google-search", "params": {"query": query, "country": country}, "queue": "live"}
    for attempt in range(retries):
        resp = requests.post(API_URL, json=body, headers=HEADERS, timeout=180)
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("retry-after", 10)))
            continue
        resp.raise_for_status()
        job = resp.json()["job"]
        if job["status"] == "completed":
            return resp.json()["result"]
        if "retry" in (job["error"] or "").lower():  # "service is busy" and "temporarily blocked" both say retry
            time.sleep(5 * (attempt + 1))
            continue
        raise RuntimeError(job["error"])
    raise RuntimeError(f"gave up on {query!r} after {retries} tries")


def run_batch(queries, workers=3):
    """Run searches in parallel. Returns ({query: result}, {query: error message})."""
    results, errors = {}, {}

    def one(q):
        try:
            results[q] = search(q)
        except Exception as e:  # keep going: one bad search must not stop the batch
            errors[q] = str(e)

    with ThreadPoolExecutor(workers) as pool:
        list(pool.map(one, queries))
    return results, errors

Three details matter:

  • It retries only what can succeed on a second try. A 429 waits as long as the retry-after header says. Transient failures whose message says to retry, such as The service is busy. Please retry shortly. and Google temporarily blocked the request. Please retry., back off 5, 10, then 15 seconds. We saw each of those messages during our testing. A validation error, such as an unsupported country, raises at once, because it would fail every time.
  • One bad search does not stop the batch. Failures go into errors, so you can look at them afterwards and re-run only those.
  • workers=3 is the default for a reason. For searches it matched the throughput of 10 workers with much lower latency per request.

Failed requests are not charged, so retrying costs nothing extra.

The fast queue, in code

def run_fast(queries, country="us", poll_every=3, timeout=300):
    """Submit all searches to the fast queue (HTTP 202), then poll until each finishes."""
    ids = {}
    for q in queries:
        body = {"endpoint": "google-search", "params": {"query": q, "country": country}, "queue": "fast"}
        resp = requests.post(API_URL, json=body, headers=HEADERS, timeout=60)
        resp.raise_for_status()
        ids[q] = resp.json()["job"]["id"]
    results, errors = {}, {}
    deadline = time.time() + timeout
    while len(results) + len(errors) < len(ids) and time.time() < deadline:
        for q, job_id in ids.items():
            if q in results or q in errors:
                continue
            resp = requests.get(f"{API_URL}/{job_id}", headers=HEADERS, timeout=30)
            job = resp.json().get("job") if resp.status_code == 200 else None
            if job and job["status"] == "completed":
                results[q] = resp.json()["result"]
            elif job and job["status"] == "failed":
                errors[q] = job["error"]
        time.sleep(poll_every)
    return results, errors

The poll reads the response defensively. In one of our runs a poll came back without the job key, and we could not reproduce it, so the function checks the status code and the key and simply tries again on the next pass, instead of crashing the batch.

We ran both on six and four searches:

live: 6 ok, 0 errors in 22s
fast: 4 ok, 0 errors in 17s

For a large list, save ids to disk after submitting. If your script stops, you can poll the same job IDs later instead of paying for the searches again.

Picking an approach

You have Use
A few hundred searches, and you want results now run_batch with 3 workers
Thousands of searches, or a job that must survive a restart run_fast, saving the job IDs
Autocomplete or other quick endpoints in bulk The live queue with more workers: we measured up to 10
Results pushed to your own service The queue options in Get Google search results as JSON with Python

What it costs

Concurrency does not change the price. Every request is 1 credit on every queue, and failed requests are not charged. Starter is $1.00 per 1,000 requests and the largest pack is $0.30 per 1,000, see pricing. The tests behind this guide cost 160 credits for the autocomplete runs, 90 for the live searches and a few dozen for the fast queue runs.

Next steps

You now have measured numbers for how the live queue behaves with more workers, a batch runner that retries only what can succeed, and a fast-queue version for large or restartable jobs. For what to do with the results, see the rank tracking guide and AI Overview tracking, and the Google Search API page for every parameter.

Frequently asked questions

How many Google searches can I run in parallel?
More workers did not make live google-search requests faster in our October 2026 tests. 30 searches took 85 seconds with 3 workers, 82 with 6 and 81 with 10, about 0.36 per second each time. Extra workers only raised the time each request took, from a median of 8.1 seconds to 22.2. Three workers is a sensible default.
Does concurrency help for every endpoint?
No. Autocomplete requests, which return in about 1.4 seconds, scaled almost linearly: 40 requests took 76 seconds with 1 worker and 7 with 10, which is 5.75 requests per second, with no errors. Searches behaved differently, so measure the endpoint you use.
Should I use the live queue or the fast queue for a large batch?
Use the fast queue to avoid holding connections open. It accepts a job with HTTP 202 straight away and you poll for the result. In our test, 10 searches were submitted in 3.1 seconds and all finished within 24 seconds. It cost the same 1 credit per request, and it was not faster than the live queue.
What errors should my code retry?
A 429 response, where you wait as long as the retry-after header says, and the transient failures whose message says to retry: The service is busy. Please retry shortly. and Google temporarily blocked the request. Please retry. Failed requests are not charged. Do not retry validation errors such as a bad country, because they will fail again.
How long will 10,000 searches take?
At the rate we measured, about 0.36 searches per second, roughly 7.7 hours. Your throughput can differ, since we measured one account on one afternoon, so run a batch of 30 first and use your own number.
Does running in parallel cost more?
No. Every request is 1 credit on every queue, however you schedule it. Parallel workers only change how long the batch takes.
  • SERP API
  • Python
  • Concurrency
  • Batch searches
  • Rate limits
  • Tutorial

Guides · 8 min read

Build a simple keyword rank tracker

Track where a domain ranks on Google for a keyword list, every day, by country and device. Python, SQLite and the CrawlerData API, with real cost math.