# Google Trends Scraper (`excellent_mustang/google-trends-scraper`) Actor

Scrape Google Trends interest over time, interest by region, related queries and topics, and trending-now — without the 429 failures. Successes are billed, failures are not, and every run finishes inside a wall-clock budget instead of hanging.

- **URL**: https://apify.com/excellent_mustang/google-trends-scraper.md
- **Developed by:** [Gorav Agarwal](https://apify.com/excellent_mustang) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Trends Scraper

Scrape **Google Trends** at scale — interest over time, interest by region, related queries, related topics and trending-now — and actually get data back.

**$2.50 per 1,000 dataset rows. No start fee. Failed requests are never billed.**

***

### The problem this Actor exists to solve

Google Trends scrapers fail, loudly and often. The most-used Google Trends Actor on this platform ran **22,254 times in 30 days and 26.1% of those runs did not produce a result** — 643 failed, 5,157 timed out, 2,301 were aborted. Its open issues are titled *"It never worked…"*, *"Runs with infinite loop and no result"*, *"Too much time and no output"*, *"keep failing"*, *"No search volumes returned"*.

That is not Google being unscrapeable. It is one specific, fixable mistake.

#### What actually goes wrong

A request to Google's `/trends/api/explore` endpoint from a client that has **no `NID` cookie** returns **HTTP 429 Too Many Requests** — on the *first* request, from a clean IP, with no prior traffic. It looks exactly like a rate limit, so scrapers treat it as one: they back off, retry, spin, and eventually time out having never made a single successful call.

It is not a rate limit. It is a cookie gate. Measured on a clean IP:

| Client | Result |
| --- | --- |
| No cookies (control) | **429** |
| `Referer` header, still no cookies | **429** |
| `NID` cookie, no `Referer` | **200** |
| Cold client per request, 20 requests | **20/20 → 429 (100%)** |
| One warmed session, reused | **0/30 → 429 (0%)** |

Google hands out an `NID` cookie to anyone who asks. One cheap GET before the first API call is the entire difference between a 100% failure rate and a 0% failure rate. This Actor does that, keeps the session warm, and rotates it through Apify Proxy when Google's separate per-IP quota does eventually bite.

***

### How it stays under the rate limit

- **Session warm-up.** Every proxy session is issued an `NID` cookie before its first API call, via a 1.6 KB endpoint rather than the 681 KB Trends home page — 425× less bandwidth per rotation.
- **Proxy rotation on 429.** A 429 means the identity is spent, so the Actor takes a new proxy session and a new cookie rather than sleeping on a dead one. Concurrent 429s coalesce into a single rotation instead of stampeding.
- **Exponential backoff with full jitter.** `sleep = uniform(0, min(cap, base · 2^attempt))` — the jitter variant that actually de-synchronises concurrent retries, rather than the naive `delay · random()` that leaves them clustered.
- **Adaptive concurrency.** Sustained throttling halves the in-flight limit and paces requests apart; sustained success walks it back up. A fixed concurrency turns a soft quota into a hard wall.
- **A hard wall-clock budget.** When the budget expires the run stops scheduling, pushes everything it already has, and finishes **successfully** with a status message saying it was partial. There is no code path that hangs.
- **A fallback that cannot be throttled.** Google's per-IP quota covers the whole `/trends/api/*` surface — but *not* the trending endpoints. Measured on a quota-exhausted IP in the same second: `explore` → 429, `widgetdata` → 429, trending API → **200 with 284 trends**. So when the keyword path is walled off, the Actor falls back to trending and still returns data. In a live test against a fully throttled IP with no proxy available, the run finished successfully with **285 rows** instead of failing.
- **Every run publishes its own rate-limit report** — observed 429 rate, rotations, recoveries, total backoff — to the `RUN_SUMMARY` record. The claim is measured, not asserted.

***

### Success-only billing

You are charged for rows in the default dataset. That is it.

Anything that fails — a throttled request, a malformed keyword, a region Google has no data for — is written to a separate **`failures` dataset** with its HTTP status code and reason, and **is not charged**. If a run returns nothing, it costs nothing. This directly answers the *"charged but returned zero results"* complaint that dogs the incumbent.

At **$2.50 / 1,000 rows with no start fee**, a 53-row single-keyword pull costs about **$0.13**. Competing Actors charge a $20 start fee that dominates any small run, or a flat $3.00/1k. Nested output mode collapses a whole series into one row if you want the bill smaller still.

***

### What you get

| Data type | Rows | Fields |
| --- | --- | --- |
| `interest_over_time` | one per keyword per bucket | `date`, `formattedTime`, `value` (0–100), `isPartial` |
| `interest_by_region` | one per keyword per region | `regionName`, `regionCode`, `value`, `hasData` |
| `related_queries` | top + rising, ranked | `query`, `listType`, `rank`, `formattedValue`, `link` |
| `related_topics` | top + rising, ranked | `topicTitle`, `topicType`, `topicMid`, `rank` |
| `trending_now` | hundreds per geo | `searchVolume`, `growthPercent`, `startedAt`, `relatedTerms`, news articles |

Every row carries **`keyword`, `geo`, `timeframe` and `fetchedAt`**, so a multi-keyword multi-region run stays unambiguous in a single flat table.

### Features

- **Timeframes** from the past hour to 2004-to-present, plus custom `YYYY-MM-DD YYYY-MM-DD` ranges. Resolution follows automatically — 8-minute buckets for the past day, hourly for the past week, weekly for a year.
- **Geo targeting** by country (`US`, `GB`, `IN`), sub-region (`US-CA`, `GB-ENG`), or worldwide. Run one keyword set across many regions in a single job.
- **Multi-keyword comparison** respecting Google's hard 5-term limit (a 6th term returns HTTP 400). Longer lists are split automatically. Because Google normalises interest *within* a comparison group, `individual` mode — one request and one 0–100 scale per keyword — is the default for batch lists, and `compare` mode tags each row with its `comparisonGroup` so the scales stay interpretable.
- **Category filter** (Finance, Health, Sports, Travel and ~1,400 more) and **search type** (web, images, news, YouTube, Shopping).
- **Trending now** from Google's current realtime feed — hundreds of trends per region with search volume, growth percentage, related terms and linked news, not the 10-item RSS cap.
- **No browser.** Plain HTTP against Google's own JSON endpoints, so runs are fast and compute is cheap.

### Input example

```json
{
  "keyword": "bitcoin",
  "keywords": ["ethereum", "solana"],
  "geo": "US",
  "additionalGeos": ["GB"],
  "timeframe": "today 12-m",
  "dataTypes": ["interest_over_time", "interest_by_region", "related_queries"],
  "comparisonMode": "compare",
  "maxRunTimeSecs": 240
}
```

### Output example

```json
{
  "position": 1,
  "keyword": "bitcoin",
  "geo": "US",
  "timeframe": "today 12-m",
  "dataType": "interest_over_time",
  "date": "2026-09-13T00:00:00Z",
  "formattedTime": "Sep 13 – 19, 2026",
  "value": 28,
  "isPartial": false,
  "category": 0,
  "searchType": "web",
  "language": "en-US",
  "fetchedAt": "2026-09-21T18:04:11Z"
}
```

### Proxy and cost

Google enforces **two separate limits**, and they need different answers:

1. **The cookie gate.** No `NID` cookie → 429 on the first request. Beaten by warming the session. Free, instant, no proxy involved.
2. **A per-IP quota**, which only appears under sustained load. Measured: 100 requests through a single IP at concurrency 4 produced 63% 429s and zero rows, and that IP then refused even a correctly warmed single request for over four minutes. Backing off does not clear this in any useful time — **only changing IP does.**

That is why a 429 here rotates the proxy session rather than sleeping on a dead one, and why concurrency is reduced under pressure instead of held constant.

**Datacenter proxy is sufficient and is the default.** Rotation, not residential bandwidth, is what defeats this limit — the constraint is IP *diversity*, not IP reputation, and the datacenter pool supplies plenty of both. Residential is therefore not required.

For completeness, if you did switch to residential it would not be the cost problem: these are JSON endpoints rather than rendered pages, so the payload is roughly **0.2–0.4 KB per dataset row**, and at $8/GB that is about **$0.002–0.003 per 1,000 rows** on top of the per-result price. Negligible either way — which is precisely why it is not worth paying for. The Actor also warms each session through a 1.6 KB endpoint instead of the 681 KB Trends home page, so rotating often stays cheap: a measured run that rotated five times spent 8.3 KB in total on warm-ups.

### Notes on Google's own behaviour

- Google returns **no related-topics widget at all** for multi-keyword comparisons. That is upstream behaviour, not a bug in this Actor.
- An empty `rankedList` is a **valid** answer from Google for a low-volume keyword, not an error. This Actor does not bill it and does not report it as a failure — a distinction the incumbent's *"No search volumes returned"* issue suggests is not always made.
- Interest values are **relative, not absolute**: 100 is the peak within the requested query, not a search count.

### Use cases

SEO keyword research and seasonality, demand forecasting, brand-versus-competitor share of search, product launch timing, content calendars, market entry sizing by region, trading and crypto sentiment signals, academic and journalistic trend analysis.

# Actor input Schema

## `keyword` (type: `string`):

The search term to analyse. Use this for a single term; use the keyword list below for batches.

## `keywords` (type: `array`):

Additional search terms. Combined with the single keyword above, de-duplicated. Google caps a comparison at 5 terms, so longer lists are split automatically — see 'Multi-keyword handling'.

## `geo` (type: `string`):

Two-letter country code (US, GB, IN, DE), a sub-region (US-CA, GB-ENG), or leave empty for worldwide.

## `additionalGeos` (type: `array`):

Run the same keywords against more regions. Each row is tagged with its own geo.

## `timeframe` (type: `string`):

How far back to look. Shorter windows return finer resolution: 'now 1-d' is 8-minute buckets, 'now 7-d' is hourly, 'today 12-m' is weekly.

## `customTimeframe` (type: `string`):

Overrides the timeframe above. Format: 'YYYY-MM-DD YYYY-MM-DD', e.g. '2024-01-01 2024-06-30'.

## `category` (type: `integer`):

Google Trends category id. 0 is all categories. Common ids: 7 Finance, 3 Arts & Entertainment, 5 Computers & Electronics, 8 Games, 12 Business & Industrial, 13 Internet & Telecom, 16 News, 20 Sports, 45 Health, 47 Autos & Vehicles, 71 Food & Drink, 174 Travel.

## `searchType` (type: `string`):

Which Google property to measure. Web search is the default.

## `dataTypes` (type: `array`):

Which datasets to pull. Each produces its own rows, tagged with dataType. Note: Google does not offer related topics for multi-keyword comparisons — that is upstream behaviour, not a failure.

## `comparisonMode` (type: `string`):

Google normalises interest WITHIN a comparison group — the top value in the group becomes 100. 'Individual' gives every keyword its own request and its own 0-100 scale, which is what a batch list almost always means. 'Compare' puts keywords into groups of 5 that are scaled against each other, and tags each row with its comparisonGroup.

## `outputMode` (type: `string`):

'Flat' emits one row per data point, ready for a spreadsheet or a database. 'Nested' emits one row per keyword per data type with the full series in a 'data' array — far fewer dataset items, so a far smaller bill.

## `trendingSource` (type: `string`):

'Auto' uses Google's realtime trending API (hundreds of trends with search volume and growth) and falls back to the RSS feed if it is unavailable. RSS returns only 10 trends but carries full news articles.

## `trendingHours` (type: `integer`):

How far back the trending-now feed should look.

## `fallbackToTrending` (type: `boolean`):

If the keyword path returns nothing at all, fall back to the trending feed, which needs no token and no session. A run that returns trending data beats a run that returns nothing. Strongly recommended.

## `maxItems` (type: `integer`):

Hard cap on billed rows. The run also clamps itself to whatever your maxTotalChargeUsd allows before it starts scraping.

## `maxRunTimeSecs` (type: `integer`):

Hard deadline for the whole run. When it expires the Actor stops scheduling work, pushes everything it already has and finishes successfully. This is what makes a partial result impossible to turn into an infinite loop. Raise it for large keyword lists.

## `maxConcurrency` (type: `integer`):

Upper bound on parallel requests. The Actor lowers this automatically when Google starts throttling and raises it again when it stops, so the default is safe to leave alone.

## `maxRetries` (type: `integer`):

Each attempt uses exponential backoff with full jitter. A 429 additionally rotates to a fresh proxy session and a fresh cookie before retrying.

## `language` (type: `string`):

Locale for labels Google returns, e.g. en-US, de-DE, es-ES.

## `timezoneOffsetMinutes` (type: `integer`):

Google's tz parameter: minutes to add to local time to reach UTC. 0 is UTC, -330 is IST, 300 is US Eastern. Only affects bucketing of sub-daily timeframes.

## `proxyConfiguration` (type: `object`):

Apify Proxy with the datacenter group is enough for Google Trends and is what this Actor is tuned for — rotation, not residential bandwidth, is what defeats the rate limit. Leave it on.

## Actor input object example

```json
{
  "keyword": "bitcoin",
  "keywords": [],
  "geo": "US",
  "additionalGeos": [],
  "timeframe": "today 12-m",
  "category": 0,
  "searchType": "",
  "dataTypes": [
    "interest_over_time",
    "interest_by_region",
    "related_queries",
    "related_topics"
  ],
  "comparisonMode": "individual",
  "outputMode": "flat",
  "trendingSource": "auto",
  "trendingHours": 24,
  "fallbackToTrending": true,
  "maxItems": 1000,
  "maxRunTimeSecs": 240,
  "maxConcurrency": 4,
  "maxRetries": 5,
  "language": "en-US",
  "timezoneOffsetMinutes": 0,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `trends` (type: `string`):

Every scraped row: interest over time, interest by region, related queries and topics, trending now.

## `interestOverTime` (type: `string`):

One row per keyword per time bucket, on Google's 0-100 relative scale.

## `interestByRegion` (type: `string`):

One row per keyword per region.

## `related` (type: `string`):

Top and rising related searches, ranked.

## `trendingNow` (type: `string`):

Currently trending searches with volume, growth and linked news.

## `failures` (type: `string`):

Per-request status code and reason for everything that did not produce data.

## `runSummary` (type: `string`):

Observed 429 rate, session rotations, recoveries, backoff seconds and billing breakdown for this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keyword": "bitcoin",
    "geo": "US"
};

// Run the Actor and wait for it to finish
const run = await client.actor("excellent_mustang/google-trends-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keyword": "bitcoin",
    "geo": "US",
}

# Run the Actor and wait for it to finish
run = client.actor("excellent_mustang/google-trends-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keyword": "bitcoin",
  "geo": "US"
}' |
apify call excellent_mustang/google-trends-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,excellent_mustang/google-trends-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/j41KmSEma75uaLyHL/builds/Lr58kuJ9yalYDPEVq/openapi.json
