Google Trends Scraper — interest over time, regions, related
Pricing
from $0.10 / 1,000 results
Google Trends Scraper — interest over time, regions, related
Google Trends data that always finishes. Interest over time, interest by region and related queries, with Top and Rising labelled separately. Hard run limit, session-aware retries and partial-result flushing — no infinite loops, no empty runs. Any keyword, country and timeframe.
Pricing
from $0.10 / 1,000 results
Rating
0.0
(0)
Developer
Juan Carlos Gutiérrez Huérfano
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
24 days ago
Last modified
Categories
Share
Google Trends Scraper — never hangs, always returns
Interest over time, interest by region, and related queries from Google Trends.
The point of this Actor is not that it fetches Trends data — plenty do. It is that it finishes. Every run has a hard limit, every retry is budgeted against it, and everything already collected is written to the dataset before the run ends. You never watch a run burn five minutes and hand back nothing.
Why runs hang elsewhere, and what this does instead
Google Trends rarely blocks scrapers by IP. It rejects requests that arrive without a
session. Verified directly: the identical request with no cookies returns HTTP 429
and an HTML error page, while the same request with a warmed cookie jar returns 200 —
and six rapid calls in a row all returned 200 with no pacing at all.
So a 429 here means "you skipped the handshake", not "you are banned". That single
distinction drives the whole retry policy:
| Response | What most clients do | What this does |
|---|---|---|
429 | Back off and retry the same dead session, often forever | Re-warm the session, then retry |
5xx | Retry | Retry with capped backoff |
Other 4xx | Retry | Fail immediately — it will not fix itself |
| Timeout | Hang, or retry without a ceiling | Abort the request, count it, move on |
On top of that:
maxRunSecondsis absolute. No request, wait or retry can extend past it. When the clock runs out the run stops cleanly and reports what it got.- Results are flushed per keyword, not at the end. A run that dies at keyword 40 still gives you keywords 1–39.
- One dead widget never sinks a keyword. If
GEO_MAPfails butTIMESERIESsucceeds, you get the timeseries and a note about the map. RUN_SUMMARYin the key-value store lists every keyword, which widgets returned what, elapsed time, and the network counters (requests, retries, re-warms, 429s, timeouts). A slow or thin run explains itself.
Two things it gets right that are easy to get wrong
Separate vs compared scales. Google's numbers are relative, always 0–100. Ask for
three terms separately and each gets its own scale — good for seeing each term's own
shape. Ask for them compared and they share one scale — good for relative size. The
values are not interchangeable, and conflating them is the most common way Trends data
gets misread. Every row carries a compared flag so you always know which you have.
Top vs Rising. Google returns two related-terms lists: top (large and steady) and
rising (small but accelerating, including "Breakout" for >5000% growth). Most scrapers
flatten them into one array and throw away the distinction that makes the widget useful.
Here every related row is labelled list: "top" | "rising" and carries isBreakout.
And when a term is too low-volume for Google to return related terms, the summary says
no data for this term rather than emitting whatever generic trending Google falls
back to. A missing answer beats a wrong one.
Input
{"keywords": ["typescript", "rust", "golang"],"compareKeywords": false,"geo": "US","timeframe": "today 12-m","property": "","includeInterestOverTime": true,"includeInterestByRegion": true,"includeRelatedQueries": true,"includeRelatedTopics": false,"maxRunSeconds": 300}
timeframe accepts Google's own strings — now 1-H, now 7-d, today 1-m,
today 3-m, today 12-m, today 5-y, all — or a custom range like
2025-01-01 2026-01-01. geo takes a country code (US, CO, MX, ES) or a
sub-region (US-CA); leave it empty for worldwide. property selects the surface:
web, images, news, youtube or froogle (Shopping).
Output
Flat rows with a dataType discriminator, so one dataset covers every widget and you
filter instead of unpacking.
{"dataType": "interest_over_time","keyword": "typescript","geo": "US","timeframe": "today 12-m","property": "","compared": false,"date": "2026-08-09","dateLabel": "Aug 9 – 15, 2026","value": 21,"isPartial": true,"scrapedAt": "2026-08-01T12:00:00.000Z"}
{"dataType": "related_query","keyword": "typescript","list": "rising","query": "typescript 6","value": 4300,"isBreakout": false}
interest_by_region rows add regionName, regionCode and hasData — the last one
distinguishes "genuinely zero interest" from "Google had too little data to say".
isPartial marks the trailing bucket that is still filling, which is the usual cause
of a phantom drop at the right edge of a chart.
What it does not do
- No personal data. Trends is aggregate search interest; nothing here identifies anyone.
- No absolute search volumes. Google does not publish them. Anything claiming otherwise is modelling, not measuring.
- No login, ever. Everything comes from public endpoints an anonymous browser can reach.
Local development
npm installnpm run buildnpm run test:local
test:local runs offline assertions — including the two parser bugs found during
development — then performs a live session probe and prints whether a cold, anonymous
handshake succeeds on your network.
Issues
If a term returns nothing you expected, open an issue with the exact input and the
RUN_SUMMARY from that run. The summary usually contains the answer.