DEV Community

Gautham Krishna
Gautham Krishna

Posted on Edited on Fully Autonomous

pytrends is archived and throws 429s. What still works for Google Trends data in 2026

I sell a Google Trends scraper, so I spend more time than I'd like poking at Google Trends from code. This week I sat down and retested every free route I know, from my laptop and from a few datacenter IPs, to see what actually works in October 2026. The paid thing I built is at the very end. Everything before it is free.

I also got my home IP blocked halfway through, which turned out to be the most useful part.

Is pytrends dead?

Sort of. The GeneralMills/pytrends repo is archived, and the last PyPI release is 4.9.2 from April 2023. In February 2025 the maintainer posted a goodbye issue telling people to stop using it. His reason wasn't only lack of time. He wrote that when Google thinks you're a bot, it gives you "slightly altered data different from the one you can see in their website".

The library still runs, though. I installed 4.9.2 today and this worked first try:

from pytrends.request import TrendReq

pytrends = TrendReq(hl="en-US", tz=0)
pytrends.build_payload(["chatgpt", "claude"], timeframe="today 3-m", geo="US")
df = pytrends.interest_over_time()
print(df.tail(3))
Enter fullscreen mode Exit fullscreen mode
            chatgpt  claude  isPartial
date                                  
2026-10-22       81      26      False
2026-10-23       75      23      False
2026-10-24       51      17       True
Enter fullscreen mode Exit fullscreen mode

Here's what each method did on 25 Oct 2026:

pytrends method What happened
interest_over_time() Works
interest_by_region() Works
related_queries() Works
related_topics() Crashes with IndexError, because Google sends back an empty list
trending_searches() Google answers 404
realtime_trending_searches() Google answers 404

So if you only need timelines, regions and related queries, pytrends still does the job. The trending calls are gone and nobody is going to fix them.

The 429s

The thing people actually search for is this one:

pytrends.exceptions.TooManyRequestsError: The request failed: Google returned a response with code 429
Enter fullscreen mode Exit fullscreen mode

I tried to trigger it on purpose and mostly couldn't. 20 keywords in a row from my home connection, about one every 2 seconds, all fine. Then 60 keywords on 20 threads through a single datacenter IP, each one asking for the timeline, the regions and the related queries. All 60 came back in 36 seconds.

Then my home IP got blocked anyway. After roughly 70 requests in 6 minutes, a couple of them full headless-browser page loads from a library I'll get to below, every request started bouncing to google.com/sorry, Google's "unusual traffic" page. pytrends reports that as the 429 above. The block was still there more than 15 minutes later, when I stopped checking.

So I can't give you a safe number of requests per minute. The block is per IP, it clearly isn't a plain request count, and once you're in it you stay in it for a while. What I'd do:

  • Build one TrendReq and reuse it, instead of a new one per keyword.
  • Go one keyword at a time with a pause. 2 seconds a call was fine for me.
  • Cache everything. Weekly data for the last 12 months doesn't change between Monday and Tuesday.
  • When you land on google.com/sorry, stop. Retrying in a loop just keeps you there.

Skip the library and call the endpoints yourself

pytrends is a thin wrapper around the JSON endpoints the Trends website calls. If you don't want an archived dependency, you can call them with requests in about 30 lines. /trends/api/explore gives you a signed token for each chart on the page, and /trends/api/widgetdata/multiline gives you the timeline for that token.

import json
import requests

BASE = "https://trends.google.com/trends/api"
s = requests.Session()
s.headers["User-Agent"] = "Mozilla/5.0"


def get_json(path, params):
    r = s.get(f"{BASE}/{path}", params=params, timeout=30)
    if r.status_code == 429 and "NID" in r.cookies:
        # the first call from a new IP often gets a 429 that sets the NID cookie
        r = s.get(f"{BASE}/{path}", params=params, timeout=30)
    r.raise_for_status()
    return json.loads(r.text[r.text.find("{"):])  # body starts with )]}' junk


req = {
    "comparisonItem": [{"keyword": k, "geo": "US", "time": "today 3-m"} for k in ["chatgpt", "claude"]],
    "category": 0,
    "property": "",
}
explore = get_json("explore", {"hl": "en-US", "tz": "0", "req": json.dumps(req)})
ts = next(w for w in explore["widgets"] if w["id"] == "TIMESERIES")
print("userType:", ts["request"]["userConfig"]["userType"])

data = get_json("widgetdata/multiline", {
    "hl": "en-US", "tz": "0",
    "req": json.dumps(ts["request"], separators=(",", ":")),
    "token": ts["token"],
})
for p in data["default"]["timelineData"][-3:]:
    print(p["formattedTime"], p["value"])
Enter fullscreen mode Exit fullscreen mode
userType: USER_TYPE_SCRAPER
Oct 22, 2026 [81, 26]
Oct 23, 2026 [75, 23]
Oct 24, 2026 [52, 17]
Enter fullscreen mode Exit fullscreen mode

The NID retry is there because Google often answers the very first request from a new IP with a 429 that sets a cookie, and the same request works right after. In one of my scraper's runs for this post, 1 of its 6 requests was exactly that 429.

Now look at userType. Google labels the request USER_TYPE_SCRAPER. I opened the same comparison on trends.google.com in Chrome and captured the website's own call, and there it says USER_TYPE_LEGIT_USER. So Google knows, which is what the pytrends maintainer was getting at.

I compared the numbers anyway. For the full days the script and the website matched exactly, 81/26 and 75/23. For the last, partial day the script said 52/17 and the website said 50/16, and pytrends had said 51/17 a few minutes earlier. That's one comparison on one day, so it doesn't prove Google never alters scraper data. For ordinary keywords I wouldn't lose sleep over it.

Where the label clearly matters is related topics. Google now sends them only to requests that come with the website's reCAPTCHA check, so every script gets an empty list. That's the IndexError in the table above.

One more snag. The top rising query for "claude" in the US was "codex vs claude code abraham quiros villalba", marked Breakout, and two more rising queries had the same name tacked on. The website shows them too. Rising queries come straight from Google with no cleanup, so filter them before they go in a report.

Trending searches: use the RSS feed

trending_searches() is dead, but Google has a public RSS feed for Trending Now, and it's the easiest thing in this post. Standard library only:

import urllib.request
import xml.etree.ElementTree as ET

NS = {"ht": "https://trends.google.com/trending/rss"}
with urllib.request.urlopen("https://trends.google.com/trending/rss?geo=US") as r:
    root = ET.parse(r).getroot()
items = root.findall("./channel/item")
print(len(items), "trends")
for item in items[:3]:
    print(item.findtext("title"), item.findtext("ht:approx_traffic", namespaces=NS))
Enter fullscreen mode Exit fullscreen mode
10 trends
kansas state vs arizona state 2000+
mi state vs ucla 5000+
boston college vs georgia tech 2000+
Enter fullscreen mode Exit fullscreen mode

You get 10 items for the US with a rough traffic bucket and a few news links each. The nice surprise is that it kept working from my home IP while the explore endpoints were blocking me.

Maintained libraries

trendspyg is the most active one I found, version 1.9.0 from 1 Oct 2026. It reads trending searches from that RSS feed and from a CSV export. For keyword data it drives a headless Chrome through the real website. My first explore call took 27 seconds and returned data. The second one raised RateLimitError, "Google Trends is blocking Explore requests from this IP". Its error message says Google allows roughly 8 to 10 fresh browser sessions per hour. To be fair, I'd already sent a lot of requests from that IP by then, so I can't say it was trendspyg that tipped it over.

trendspy, which the pytrends maintainer pointed people to in 2025, hasn't had a release since December 2024.

The official Google Trends API

Google announced one in July 2025. As of today the page still says Google is "now accepting applications for alpha testers", with a form to apply. I don't have access, so I can't tell you how it behaves. On paper it has 5 years of rolling data, daily to yearly steps, regions, and values that aren't squashed to 0 to 100, so you can merge separate requests. If you need Trends data for something that has to keep running for years, apply. It's the only route here that isn't scraping.

BigQuery

Google also publishes a public BigQuery dataset, bigquery-public-data.google_trends. It holds the top 25 and top 25 rising searches for each US metro area, refreshed daily, plus tables for other countries. It's good for "what's hot in Denver this week". It can't look up your keyword, and I didn't run it for this post.

When I'd stop doing it myself

For a notebook, pytrends or the raw calls above are fine. Keep the volume low and cache what you get.

It gets annoying when it's a job that runs every day on a server. Then you're handling blocked IPs, retries, and endpoints that change without notice, like the trending calls that broke in pytrends.

That's why I built google-trends-scraper. It's mine, it runs on Apify, and it's paid per result, $1 per 1,000 keyword comparisons of up to 5 keywords each. Comparisons that fail or have no data aren't charged. When Google refuses a request, it retries on fresh residential IPs for about two minutes before it gives up on that comparison. It isn't the only paid option, SerpApi has a Google Trends API too.

import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("heyitsgautham/google-trends-scraper").call(run_input={
    "comparisons": ["chatgpt, claude"],
    "geo": "US",
    "timeRange": "today 3-m",
}, logger=None)
for row in client.dataset(run.default_dataset_id).iterate_items():
    if row["type"] == "interestOverTime":
        print(row["summary"])
        for p in row["timeline"][-3:]:
            print(p["formattedTime"], p["values"])
    elif row["type"] == "relatedQueries":
        print(row["keyword"], "rising:", [q["query"] for q in row["rising"][:3]])
Enter fullscreen mode Exit fullscreen mode
93 points, averages chatgpt 68, claude 19
Oct 22, 2026 {'chatgpt': 80, 'claude': 27}
Oct 23, 2026 {'chatgpt': 75, 'claude': 24}
Oct 24, 2026 {'chatgpt': 52, 'claude': 17}
chatgpt rising: ['sam altman chatgpt water usage', 'chatgpt astra', 'chatgpt student offer']
claude rising: ['codex vs claude code abraham quiros villalba', 'apple smartring abraham quiros villalba', 'abrahamquirosvillalba .com']
Enter fullscreen mode Exit fullscreen mode

Notice Oct 22 says 80/27 here and 81/26 above. Google samples its data, so the same query moves by a point between requests, on the website too. My scraper doesn't escape anything in this post. It reads the same unofficial endpoints, Google labels it a scraper too, and it can't get related topics either. The apify_client code above is for version 3 of the client, which returns run.default_dataset_id instead of a dict.


This article was created with the help of AI. An AI agent (Claude) ran every test above on my machine on 25 Oct 2026 and drafted the text from those runs, and every output block is copied from them.

Top comments (0)