I run a visa agency in Bali, which means two Google News searches decide a surprising amount of my week: Bali and Indonesia visa. Immigration rules here move by press conference, and the version that reaches me through WhatsApp forwards is usually three days old and half wrong. So I built an Apify Actor that reads Google News feeds — searches, topic sections, publisher filters, any language/country edition — and returns one row per headline with the publisher's own URL instead of the news.google.com redirect.
It has been pointed at the dullest possible input on a schedule ever since:
{ "queries": ["Bali", "Indonesia visa"], "maxArticlesPerQuery": 10, "resolveUrls": true, "language": "en", "country": "US" } Last night I stopped checking exit codes and opened the datasets instead. 41 runs with byte-identical input between 6 October and 1 November 2026, 820 article rows, 165 distinct article URLs across 110 domains. Every run returned exactly 20 rows. Every single one of the 820 rows resolved to a publisher URL — zero resolveError. Nothing was null except snippet, which was null 820 times out of 820.
That's the good news, and it is also what made the bad news hard to spot: the field that broke never looks broken.
publishedAt is never missing, and 46% of the time it is not a time
rows whose publishedAt ended in exactly 07:00:00.000Z 374 of 820 (46%) rows whose publishedAt ended in 00:00 (mm:ss) 388 of 820 rows whose publishedAt had seconds == 00 509 of 820 rows at 08:00:00.000Z 0 07:00:00.000Z is midnight in UTC−7 — midnight in California, where Google keeps its clocks, through the whole of October. So those 374 rows are not timestamps at all. They are a date wearing a timestamp's formatting, and the Actor faithfully hands you the ISO string Google put in the feed.
Then I bucketed the same 820 rows by how old the article was at the moment I read it:
article age at scrape rows at 07:00:00.000Z under 1 day 112 0 (0%) 1 to 3 days 112 1 (1%) 3 to 7 days 145 0 (0%) 7 to 30 days 170 92 (54%) over 30 days 281 281 (100%) The precision of publishedAt is not a property of the article or of the publisher. It is a function of the article's age. Under a week old, Google tells you the second. Past a month, it has never once told me anything but the date — 281 out of 281. The lone exception in the fresh buckets was a PR Newswire release 2.9 days old, which is its own kind of signal.
The degradation runs one way
Because the input never changed, I could follow single URLs across runs. 14 of the 165 URLs changed their publishedAt at least once, and the transitions are lopsided: 11 went from a precise timestamp to 07:00:00Z, exactly 1 went back the other way. Articles don't get re-dated; they get rounded off.
nypost.com …wild-scenes-emerge-from-bali… 21 Oct run 2026-10-14T15:34:00.000Z 23 Oct run 2026-10-14T07:00:00.000Z designboom.com …oma-potato-head-good-times-general-store-bali… 24 Oct run 2026-10-23T11:58:28.000Z 31 Oct run 2026-10-23T07:00:00.000Z apnews.com …indonesia-bali-fuel-shortage-long-lines… 18 Oct run 2026-10-14T16:11:00.000Z 19 Oct run 2026-10-16T21:37:51.000Z 21 Oct run 2026-10-14T16:11:00.000Z 23 Oct run 2026-10-14T07:00:00.000Z 30 Oct run 2026-10-16T07:00:00.000Z The AP row is the ugly one: same URL, same article id, flip-flopping between two publication times two days apart, and then both of them collapsing to their respective midnights. Whatever Google is serving there, it is a cache with more than one opinion.
The practical rules I now follow, and would hand to anyone building on this source:
- Never diff
publishedAtto detect an update. You will record "this article moved 8 hours earlier" when all that happened is that it turned seven days old. - Capture your own
firstSeenAtthe first time a URL appears in your store. It is the only monotonic clock in the pipeline. The Actor gives youscrapedAton every row for exactly this. - Treat
mm:ss == 00:00as a precision flag. If you need hour-of-day — publishing-rhythm charts, "news breaks at 9am" claims, event studies against market opens — drop those rows instead of letting 46% of your sample pile up at 07:00. - Backfilling history gives you dates, not times.
dateFrom/dateTowalks you through years of archive, and all of it will be midnight-in-California. Plan the analysis around days.
One counter-example, and it is instructive. A DW story about a Swiss tourist jailed in Bali kept a precise timestamp and kept moving it forward:
30 Oct 2026-10-28T18:18:33Z 31 Oct 2026-10-28T18:41:03Z 31 Oct 2026-10-30T22:28:17Z 31 Oct 2026-10-31T15:43:05Z 01 Nov 2026-11-01T03:07:20Z Same URL, same headline, five publication times over three days. That is a live-updated story, and the forward march is real editorial activity rather than rounding. So publishedAt is not junk — it just answers two different questions depending on age, and never says which.
The article id is not a key. The URL is
I had assumed articleId — the CBMi… token in the Google link — identified a story. Two URLs out of 165 proved otherwise:
news.com.au …run-out-wild-scenes-are-emerging-out-of-bali… 19 Oct id length 283 "Wild scenes are emerging out of Bali" 20 Oct id length 283 "Wild scenes are emerging out of Bali" 20 Oct id length 571 "'Run out': Wild scenes are emerging out of Bali" The 571-character id is the 283-character id plus a suffix: same prefix for the first 282 characters, then it grows. It happened on the run where the publisher edited the headline. The SCMP row did the same thing, 268 characters to 535, on a headline edit of its own. Going the other direction, zero ids ever pointed at two different URLs, and no run ever returned the same URL twice.
So: articleId and googleUrl are versioned tokens that change when the story changes, and articleUrl is the identity. This is the whole reason the Actor resolves the redirect by default rather than leaving you the Google link — a dedupe key that mutates on a headline edit will hand you the same story twice and call it breaking news. 4 of my 165 URLs also had their title rewritten under a stable URL, so don't key on the headline either.
The resolution is two extra requests per article and a separate charge event, and the run-summary row the Actor writes at the end told me what one of these runs actually cost: 20 articles at $0.0014 and 20 resolved URLs at $0.0009, $0.046 for the run. Resolving is the cheaper half of a row and it is the half that makes the row joinable.
A narrow query returns an archive, not a feed
The last thing the panel showed me had nothing to do with fields. The two queries behaved like different products:
unique URLs median row age rows over 30 days old "Bali" 119 3.3 days 62 of 410 (15%) "Indonesia visa" 46 49.1 days 219 of 410 (53%) maxArticlesPerQuery: 10 on Indonesia visa does not return the ten latest articles about Indonesian visas. It returns the ten things Google can find, most of which are old, because there simply aren't ten fresh ones. One article — a VOA fee story from 12 August — appeared in all 41 runs, 27 days running. Across consecutive runs the 20-row result set overlapped by 11.9 rows on average, and 66 of my 165 URLs showed up in exactly one run and never again.
So a narrow query polled hourly pays for the same archive over and over: poll it daily and spend the budget on more query variants instead. And "it's in the top 10" is not freshness — filter on publishedAt yourself, with the precision caveat above.
Watch for syndication too. 4 of my titles appeared on 2 to 4 distinct URLs each — one SPUN visa-platform press release landed on usatoday.com, streetinsider.com, local3news.com and daily-tribune.com, four separate articles, four billed rows, one text. Deduping by title would wrongly merge different outlets covering one story; deduping by URL leaves four copies of a press release. I key on URL and flag title collisions for review.
Reading one yourself
curl -s -X POST "https://api.apify.com/v2/acts/lergassy~google-news-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"queries":["Bali"],"maxArticlesPerQuery":10,"resolveUrls":true}' The freshest row from the 1 November run, untouched:
{ "type": "article", "query": "Bali", "title": "Influencer Khiana Smith in ICU with Life-Threatening Brain Infection During Bali Vacation", "source": "TMZ", "sourceUrl": "https://www.tmz.com", "publishedAt": "2026-11-01T17:03:25.000Z", "articleUrl": "https://www.tmz.com/2026/11/01/khiana-smith-diagnosed-with-bacterial-meningitis/", "articleDomain": "tmz.com", "snippet": null, "relatedArticles": [], "scrapedAt": "2026-11-01T20:26:31.679Z" } Three hours old, timestamped to the second. Give it five weeks and it will say 2026-11-01T07:00:00.000Z.
Two more honest numbers from the panel, since the point of keeping it is to publish what it says. articleDomain disagreed with the host in sourceUrl on 0 of 820 rows — Google's attribution and the resolved link never contradicted each other, which is the single most reassuring thing I found. And relatedArticles was non-empty on 2 rows out of 820: that cluster of other-outlet coverage is a topic-feed field, so if you want it, pass topics, not queries. The README says as much; now I have the count.
The panel also caught a fix landing. The 18 runs before 19 October took a median of 47 seconds (worst: 80); after I switched requests to go direct first and fall back to the datacenter proxy only on refusal, the next 23 ran in a median of 5 seconds. 4 of those 23 still took 34 to 59 seconds, so set your timeout for a minute.
When not to use this
If you need article bodies, this is the wrong tool — it returns headlines, sources, times and links, and the bodies belong to the publishers. If you need more than 100 results for one query, Google News won't give them to you at any price; split by date windows or by site:. If you need an exact publication minute for anything older than a month, no Google News client can help you, including this one — you have to fetch the publisher's page and read its own metadata. And if your query is as narrow as Indonesia visa, accept that you have built a watchlist, not a firehose.
What it is good for is the thing I actually need: a daily, deduplicated, publisher-linked table of everything written about a place or a rule change, in any edition, for fractions of a cent per headline. The Actor is Google News Scraper on the Apify Store, and the 41-run panel above is just its health check, which turns out to be the most useful dataset I own.

