I run a visa agency in Bali, and the other half of my week goes into Apify Actors. One of them reads public Facebook Pages without a login — the logged-out page HTML, parsed into one flat row per page: name, category, followers, "talking about this", check-ins, intro text, public e-mail, phone, website, address with coordinates, opening hours, price range, recommendation rating.
Then a buyer did something more useful than any test I had written. They pointed the Actor at the three pages in the input prefill — nike, shakeshack, balizoopark — and scheduled it nightly. Twenty-four runs between 29 September and 27 October 2026, 72 page rows, same three URLs, mostly around 00:20 UTC with a few extra runs during the day.
That is a month-long panel of the same rows, which is the only way to find out which columns in a scraper's output are measurements and which are weather.
The month
| Page | followers, 29 Sep → 27 Oct | check-ins | steps that went up |
|---|---|---|---|
| nike | 39,516,065 → 39,495,518 (−20,547) | 18,400 → 18,673 (+273) | 3 of 23 |
| shakeshack | 474,125 → 474,686 (+561) | 158,121 → 159,402 (+1,281) | 14 of 23 |
| balizoopark | 689 → 690 (+1) | 2,769 → 2,767 (−2) | 3 of 23 |
Nike's Facebook Page lost 20,547 followers in 28 days, and it lost them steadily: 20 of 23 consecutive snapshots were lower than the one before. Nobody tells you this, because −0.05% a month is invisible unless you are storing the number every night.
Shake Shack is the opposite shape and the more interesting one. Followers grew 0.12% while check-ins grew 0.81% — seven times faster in relative terms. Check-ins are a lifetime cumulative counter, so they can only go up, and they went up on 22 of 23 steps. If you want a Facebook-side proxy for "are people physically showing up", this column is it, and it is the one nobody charts.
Bali Zoo Park's row is a reminder that counters are not ledgers. Its check-ins went 2,769 → 2,767 and its followers wobbled 689 → 691 → 690 over the month. A 690-follower page moves ±1 as a matter of course, which is 0.3% noise on a number you might be using to detect growth.
The counter that argues with itself
talkingAbout is the one everybody reaches for, because it is the only engagement signal on a logged-out page. Over the month, Nike's ranged from 69,844 to 257,979 — a factor of 3.69. Shake Shack's ranged 3,632 to 10,090, a factor of 2.78.
A fast-moving metric is fine. The problem shows up on the days the buyer happened to run more than once:
nike, 21 October 00:20 157,615
05:22 140,877
09:02 148,138 spread 16,738 (10.6%)
shakeshack, 22 Oct 00:19 4,650
03:25 4,393
09:27 3,937 spread 713 (15.3%)
For Nike, the median overnight change across 17 midnight snapshots was 13,109. The spread inside 21 October was 16,738 — bigger than half the day-to-day moves you would have reported as news. For Shake Shack the median overnight change was 333 and the 22 October intraday spread was 713, more than double.
So I ran the degenerate version of the experiment. The Actor de-duplicates its input list by string, not by resolved page, so five different spellings of the same page all get fetched separately, in parallel, in one run:
{
"pages": ["https://www.facebook.com/nike", "nike", "facebook.com/nike",
"www.facebook.com/nike", "https://www.facebook.com/nike/",
"shakeshack", "https://www.facebook.com/shakeshack",
"facebook.com/shakeshack"],
"concurrency": 8
}
Eight rows, eleven seconds, $0.04. All five Nike rows resolved to the same pageId, 15087023444:
| scrapedAt | followers | checkins | following | talkingAbout |
|---|---|---|---|---|
| 13:10:12.729 | 39,494,184 | 18,705 | 24 | 147,167 |
| 13:10:13.058 | 39,494,184 | 18,705 | 24 | 147,167 |
| 13:10:14.804 | 39,494,184 | 18,705 | 24 | 146,754 |
| 13:10:14.940 | 39,494,184 | 18,705 | 24 | 146,754 |
| 13:10:15.604 | 39,494,184 | 18,705 | 24 | 146,754 |
Followers: identical to the digit, five times. Check-ins: identical. following: identical. talkingAbout: two different values 1.7 seconds apart, and they cluster — two reads on one number, three on another, which looks like two caches rather than two samples. Shake Shack did the same thing in the same run: 474,689 followers and 159,405 check-ins on all three reads, talkingAbout of 3,653 on one and 3,759 on the other two, a 2.9% disagreement inside six seconds.
Every one of these numbers comes out of the same og:description tag in the same HTML document. Facebook serves them together and only one of them is reproducible.
What that means for the dashboard
Treat talkingAbout as a sample, not a reading. One nightly value is a draw from a distribution with a 3–15% width. Reading the page three times and taking the median costs $0.015 and is a genuinely different number. Comparing single reads week over week is reporting cache state.
null is not zero. Bali Zoo Park returned talkingAbout: null in 7 of 24 snapshots, and the sequence went 2, 2, null, null, 1 — the field vanishes from og:description when the figure is tiny and reappears when it is not. Coalescing null to 0 would have drawn a graph that drops to the floor and recovers four days later. Nothing happened.
likes is dead. All 72 rows: null. Facebook now prints "X followers" and no longer prints a like count for these pages. If your schema still has a likes column as the primary audience metric, it is a column of nulls.
The rating is nearly frozen. Bali Zoo Park held "92% recommend (259 reviews)", unchanged, in all 24 snapshots. Daily polling for review movement on a small page is 24 reads to learn nothing; weekly is plenty.
Two field-level warnings from the same rows, both of which are Facebook's data and not the parser's:
{
"name": "Bali Zoo Park Ticket",
"category": "Zoo",
"address": "Singapadu, Sukawati, Gianyar, Bali 80582, Mbali, Democratic Republic of the Congo",
"latitude": -8.59404852,
"longitude": 115.26838303,
"phone": "+81 23-619-4398",
"email": "thebalipackage@gmail.com",
"hours": "Always open",
"priceRange": "$$"
}
The coordinates are correct — that is Singapadu in Gianyar, Bali. The address string Facebook renders next to them ends in the Democratic Republic of the Congo. Geocode from latitude/longitude, never from the formatted string. And the phone fields are whatever the page owner typed: this Bali zoo reseller publishes a +81 Japanese number, and facebook.com/nike publishes +972 54-292-7777, an Israeli mobile, every single night for a month. Those are valid, parseable, fully populated values. They are not a company switchboard.
Running it
The Actor is Facebook Pages Scraper — $0.005 per page, no run fee, no proxy charge, and pages that fail or fall under your follower filter are not billed. From Python, three reads and a median:
from statistics import median
from apify_client import ApifyClient
client = ApifyClient("<YOUR_API_TOKEN>")
page = "shakeshack"
run = client.actor("lergassy/facebook-pages-scraper").call(run_input={
"pages": [page, f"facebook.com/{page}", f"https://www.facebook.com/{page}"],
"concurrency": 3,
})
rows = [r for r in client.dataset(run["defaultDatasetId"]).iterate_items()
if r["type"] == "page"]
talk = [r["talkingAbout"] for r in rows if r["talkingAbout"] is not None]
print(rows[0]["followers"], rows[0]["checkins"], # exact
median(talk) if talk else None, len(talk), "reads") # sampled
The type filter matters: the last row of every run is a free summary row carrying the billing ({"type": "notice", ..., "chargedTotalUsd": 0.0135}), and it has no followers field to average.
One thing the login wall does to this job: for a logged-out visitor, a page that does not exist, is unpublished or is age-restricted all come back as the login page with HTTP 200. The Actor retries those on fresh datacenter addresses, then residential, then an unblocker, and anything still stuck arrives as a free error row with a reason — but "we could not open it" and "it is gone" are not distinguishable from outside. Expect to confirm deletions by hand.
When it's the wrong tool
Posts, comments, reviews and ads are not on the logged-out page; that needs a logged-in view and a different tool. There is no history in this data either — Facebook publishes today's counters and nothing else, so a month-long series exists only because somebody stored 24 nightly snapshots. If you want to answer "did this page grow this quarter", start saving rows today; there is no backfill to buy.
And the portable lesson has nothing to do with Facebook. Four columns arrived in one document, from one request, parsed by one function. Three of them are reproducible to the digit across parallel reads and one is not. Before you chart any scraped counter, fetch the same object three times in the same second and diff the rows. The columns that disagree with themselves are the columns your trend line is made of.
Top comments (0)