# Google Trends Scraper — cached, no timeouts (`leonguyen2808/google-trends-cached`) Actor

Interest over time, by region, related queries and Trending now. Built for the one thing that breaks Trends scrapers: rate limits. Caches every result, paces itself, and rotates IP instead of sleeping — so runs finish.

- **URL**: https://apify.com/leonguyen2808/google-trends-cached.md
- **Developed by:** [Leo Nguyen](https://apify.com/leonguyen2808) (community)
- **Categories:** SEO tools, Open source
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Trends Scraper (cached, reliable)

Interest over time, interest by region, related topics/queries, and **Trending now** from Google
Trends. No API key, no browser.

### Why another Google Trends Actor

Because runs on this data source do not finish. Apify publishes every public Actor's run stats, and
over the last 30 days the most-used Google Trends Actor recorded:

| Outcome | Runs | Share |
|---|---|---|
| Succeeded | 15,324 | **70.5%** |
| **Timed out** | 3,984 | **18.3%** |
| Failed | 786 | 3.6% |
| Aborted by the user | 1,646 | 7.6% |
| **Total** | **21,740** | |

Nearly **22% of runs returned nothing** — and the largest single category is *timeouts*, not errors.
That is the signature of one thing: Google Trends rate-limiting, waited out until the clock ran out.

This Actor is built around that one problem. It **caches every result** and **paces itself under one
request per second**, so the limit is approached far more slowly — and when a 429 does arrive it
**rotates to a fresh IP instead of sleeping**, because a backoff on Apify is billed idle time that
still ends in a timeout.

Being straight with you: this Actor is new, so it has no 30-day success rate of its own to show yet.
What is measured below is the *rate limit itself* — where it triggers, and how long it lasts.

### What you get

| Data | Field | Notes |
|---|---|---|
| Interest over time | `points[]` | `date`, `timestamp`, `value` (0–100), `isPartial` |
| Interest by region | `regions[]` | `geoCode`, `geoName`, `value`, `hasData` |
| Related topics | `top[]`, `rising[]` | `query`, `type`, `value`, `formattedValue`, `link` |
| Related queries | `top[]`, `rising[]` | same shape |
| Trending now | `trends[]` | `term`, `geo`, `startedAt`, `searchVolume` |
| Comparison | `points[].values` | `{term: value}` per point — normalised across terms |

Every item also carries `fromCache` (boolean) so you always know whether you got a fresh fetch
or a cached one.

### The rate limit, measured

Measured against the live endpoints on 2026-08-06 from a plain datacenter IP:

- A burst with no pacing was **rate-limited at request 93 in 13.9 s** (~6.6 req/s). The 429 had
  not cleared 20 s later; it had cleared by 60 s.
- At a **1.2 s gap between requests, 30 consecutive requests passed clean.**
- The budget is **per IP and it accumulates**: after roughly 130 *paced* requests spread over hours,
  the same address was refused on the very first request of a fresh session. So pacing alone cannot
  carry production volume — caching and IP rotation are not optimisations here, they are the
  mechanism.

So this Actor paces itself under 1 request/second by default and caches every result. Two runs
asking for the same term, location and range within the cache window cost **one** request to
Google, not two. Google Trends is weekly-resolution for most ranges, so a cached answer from an
hour ago is not a worse answer — it is the same answer.

Consequences you should expect:

- **A large keyword list is slow on purpose.** Roughly 1.5 s per request, and each keyword costs
  one `/explore` plus one request per data type you asked for.
- **A failed keyword does not fail the run.** It is returned as an item with an `error` field, so
  a 100-keyword job gives you 99 good rows instead of nothing.
- Set `cacheMaxAgeMinutes: 0` if you genuinely need a live fetch every time. You will get more
  429s. That is the trade.
- **`relatedQueries` / `relatedTopics` are the first thing Google refuses.** Measured: that endpoint
  exhausted the per-IP budget while the interest-over-time endpoint on the same session still
  answered. If you need related data at volume, run on a plan with more proxy IPs — the shared
  datacenter pool on the free plan is a handful of addresses, and this Actor can only rotate between
  the ones it is given.
- **Rotation is bounded on purpose.** Each retry on a new IP still costs its pacing delay and its
  request time, and Apify bills compute per second — a run allowed to rotate indefinitely can spend
  more than its results are worth. After a set number of rotations you get an error row rather than
  an ever-growing bill.

### Limits — read before buying

- **Values are relative, not absolute.** Google returns 0–100 scaled *within one request*. So
  terms in `keywords` are fetched separately and their numbers are **not comparable to each
  other**. When you need comparison, use **`compareKeywords`** (up to 5 terms in one request):
  those values ARE normalised against each other, and it is cheaper too — 3 terms cost 2
  requests instead of 6. Google itself caps a comparison at 5; a 6th term is silently dropped,
  so this Actor refuses it rather than returning a result quietly missing a term you paid for.
- **`searchVolume` on Trending now is Google's own rounded bucket** (e.g. 100000, 1000000), not a
  precise count.
- The older `dailytrends` / `realtimetrends` endpoints Google retired now return 404. This Actor
  uses the current endpoint. If Google rotates it again, the Actor reports a clear
  "endpoint unavailable" error instead of returning empty results that look like real data.
- Google Trends has **no official public API** and its Terms of Service restrict automated
  access. You are responsible for how you use the output.
- No personal data is collected — Trends returns aggregate search interest only.

### Input

```json
{
  "compareKeywords": ["bitcoin", "ethereum", "solana"],
  "geo": "US",
  "timeframe": "today 12-m"
}
```

…returns one item whose every point carries all three terms on one scale:

```json
{ "date": "Aug 2 – 8, 2026", "values": { "bitcoin": 21, "ethereum": 3, "solana": 3 } }
```

Or fetch terms independently (values not comparable across terms):

```json
{
  "keywords": ["bitcoin", "ethereum"],
  "geo": "US",
  "timeframe": "today 12-m",
  "dataTypes": ["interestOverTime", "relatedQueries"],
  "includeTrendingNow": true,
  "trendingGeo": "VN",
  "cacheMaxAgeMinutes": 360
}
```

`timeframe` accepts Google's own range strings: `today 12-m`, `today 5-y`, `now 7-d` (hourly
points), `all` (since 2004), or an explicit `2025-01-01 2025-12-31`.

`geo` is a country code (`US`, `VN`, `GB`) or a sub-region (`US-CA`). Empty means worldwide.

### Output

One dataset item per keyword × data type, plus one item for the trending feed:

```json
{
  "keyword": "bitcoin",
  "dataType": "interestOverTime",
  "geo": "US",
  "timeframe": "today 12-m",
  "fromCache": false,
  "pointCount": 53,
  "points": [{ "date": "Aug 3 – 9, 2025", "timestamp": "1754179200", "value": 36, "isPartial": false }]
}
```

### Development

```bash
pip install -r requirements.txt
mkdir -p storage/key_value_stores/default
echo '{"keywords":["bitcoin"],"geo":"US"}' > storage/key_value_stores/default/INPUT.json
python -m src
```

`src/trends.py` is plain standard library and can be exercised on its own, without the Apify
SDK — useful when checking whether Google changed something:

```bash
python -c "from src.trends import TrendsClient; c=TrendsClient(); print(list(c.explore('bitcoin','US')))"
```

# Actor input Schema

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

Terms to look up INDEPENDENTLY, one per line. Values are scaled 0-100 within each term's own request, so they are NOT comparable across terms — use `compareKeywords` for that.

## `compareKeywords` (type: `array`):

Up to 5 terms fetched in ONE request. Google scales 0-100 within a single request, so these values ARE directly comparable to each other — unlike terms in `keywords`, which are fetched separately and are not. Also cheaper: 3 terms cost 2 requests instead of 6.

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

Two-letter country code (US, VN, GB) or a sub-region like US-CA. Leave empty for worldwide.

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

Google Trends range string. 'today 12-m' = last 12 months (weekly points), 'today 5-y', 'now 7-d' (hourly), 'all' since 2004, or an explicit '2025-01-01 2025-12-31'.

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

Each selected type is one extra request to Google per keyword, and one billed result.

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

Google Trends category id. 0 = all categories.

## `includeTrendingNow` (type: `boolean`):

Currently trending searches for the location, with search volume. This uses Google's current endpoint — the older daily/realtime trends APIs were retired and now return 404.

## `trendingGeo` (type: `string`):

Defaults to the Location above, or US.

## `cacheMaxAgeMinutes` (type: `integer`):

Google Trends is weekly-resolution for most ranges, so a recent cached answer is identical to a fresh one — and serving it means no request to Google, which is why this Actor rarely fails. Set 0 to force a live fetch.

## `minRequestGapSeconds` (type: `string`):

Default 1.2s. Measured: a burst faster than ~6.6 req/s is rate-limited for about a minute. Lower this only if you know what you are doing.

## `useApifyProxy` (type: `boolean`):

Strongly recommended. Google budgets requests per IP over hours, not seconds: a single IP was measured going permanently rate-limited after ~130 paced requests. Without a proxy, larger jobs will return errors for the later keywords.

## `apifyProxyGroups` (type: `array`):

Leave empty for datacenter proxies (cheapest). Use RESIDENTIAL only if datacenter IPs get blocked — it costs far more per GB.

## `apifyProxyCountry` (type: `string`):

Optional two-letter code. Trends results do not depend on the requesting IP's country (the `geo` input controls that), so leave empty unless a specific country's IPs are less blocked for you.

## `rotateAfterKeywords` (type: `integer`):

A new proxy session (new IP + new cookie) every N keywords. Lower spreads the per-IP budget wider; higher reuses a warmed session.

## Actor input object example

```json
{
  "keywords": [
    "bitcoin",
    "ethereum"
  ],
  "compareKeywords": [],
  "geo": "",
  "timeframe": "today 12-m",
  "dataTypes": [
    "interestOverTime"
  ],
  "category": 0,
  "includeTrendingNow": false,
  "trendingGeo": "US",
  "cacheMaxAgeMinutes": 360,
  "minRequestGapSeconds": "1.2",
  "useApifyProxy": true,
  "apifyProxyGroups": [],
  "apifyProxyCountry": "",
  "rotateAfterKeywords": 5
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `resultsCsv` (type: `string`):

No description

## `cache` (type: `string`):

No description

# 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 = {
    "keywords": [
        "bitcoin",
        "ethereum"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("leonguyen2808/google-trends-cached").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 = { "keywords": [
        "bitcoin",
        "ethereum",
    ] }

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

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

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

```

## CLI example

```bash
echo '{
  "keywords": [
    "bitcoin",
    "ethereum"
  ]
}' |
apify call leonguyen2808/google-trends-cached --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=leonguyen2808/google-trends-cached",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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