I run a visa agency in Bali, and the part of my week that is not visas goes into Apify Actors. One of them reads TikTok videos: you hand it video links or usernames and it returns one row per video — views, likes, comments, shares, saves, publish time, duration, caption, hashtags, music, subtitles, and the author with followers. No account, no cookies, no browser.
It has a health check attached that reads the same two demo videos twice a day, which means I accidentally own a logged corpus: 42 runs between 6 October and 7 November, 123 video rows, 24 distinct videos, one of them read 35 times. Today I added a fresh pull — two video links plus five of one profile's latest, 6 rows in 4.0 seconds for $0.006 — and sat down to actually look at the numbers instead of just checking that rows exist.
They are not the numbers I thought I was selling.
492 counters, not one of them with five significant figures
Here is the top of the corpus, one row per distinct video:
author views likes comments shares saves zachking 2,400,000,000 29,600,000 788,400 1,100,000 998,888 zachking 67,900,000 14,000,000 105,200 561,900 1,015,591 zachking 20,500,000 3,200,000 7,405 172,000 191,186 zachking 17,100,000 1,700,000 12,500 38,100 77,902 zachking 12,300,000 792,900 4,171 10,200 27,760 duolingo 5,900,000 836,300 4,979 191,100 55,873 duolingo 2,200,000 445,700 7,152 81,200 50,070 duolingo 656,500 78,800 1,644 4,062 8,409 nike 165,300 3,213 154 50 178 duolingo 113,300 4,510 304 151 604 Look down the first four columns and then down the fifth. Across all 123 rows I have 492 values of views, likes, comments and shares, and every single one of them has at most four significant figures. saves breaks that rule in 39 of 123 rows: 1,015,591. 191,186. 27,760. 8,409.
Four significant figures is not a rendering choice in my code — I never round anything. It is what TikTok hands over. Below 10,000 you cannot see it, which is why Nike's small video reports likes: 3,213 and looks perfectly exact; above 10,000 the tail is gone.
Why saves survives: it is the only string in the block
The video detail page carries a JSON blob, and inside it, two stats blocks for the same video. Fetched raw a few minutes ago for Zach King's 2019 broomstick illusion:
"stats": { "diggCount": 29600000, "shareCount": 1100000, "commentCount": 797000, "playCount": -1894967296, "collectCount": "999139" } "statsV2": { "diggCount": "29600000", "shareCount": "1100000", "commentCount": "797000", "playCount": "2400000000", "collectCount": "999139", "repostCount": "0" } Two things in there are worth a note on your wall.
The first is playCount: -1894967296 in the older block. That is 2,400,000,000 run through a signed 32-bit integer; add 2³² and you get it back. The statsV2 block ships the same counter as a string, so it arrives intact. Any TikTok parser should read V2 and treat a negative view count as an overflow to repair, not a parse error to drop.
The second is quieter and matters more: in stats, where every other value is a bare JSON number, collectCount is quoted. It is the only string in the block. And it is the only counter in the entire payload that is not rounded. Views, likes, comments and shares are display numbers, pre-rounded to four figures before they ever leave TikTok. Saves is a raw integer that someone plumbed straight through, typed as a string so it would be safe past 2³¹, and apparently never passed through the rounding step the others go through.
So the field nobody puts in a campaign report is the only one you can do arithmetic on.
The error is 0.05% of the level and 100% of the change
Rounding to four figures sounds harmless: the worst case is half a step, about 0.05% of the number. That is true, and it is the wrong way to think about it, because you do not report the level — you report the change. Here are six consecutive daily reads of the same Nike video, exactly as the Actor returned them:
read (UTC) views Δ likes comments shares saves 2026-10-17 134,100 – 18,200 266 1,080 1,012 2026-10-18 134,500 +400 18,200 265 1,082 1,013 2026-10-19 135,000 +500 18,300 266 1,082 1,018 2026-10-20 135,300 +300 18,300 266 1,083 1,023 2026-10-21 135,700 +400 18,300 266 1,086 1,024 2026-10-22 136,000 +300 18,300 265 1,086 1,025 Views sit at 136,000, so the quantum is 100 and a day of real growth (300–500) clears it comfortably. Likes sit at 18,300, so the quantum is also 100 — but this video earns about 28 likes a day, so the column reads 18,300 four days running and then steps. Saves, on exactly the same days, moves every day: +1, +5, +5, +1, +1. Over all 35 reads of this video:
field distinct values unchanged read-to-read views 29 / 35 6 of 34 likes 9 / 35 26 of 34 comments 12 / 35 14 of 34 shares 13 / 35 22 of 34 saves 21 / 35 13 of 34 Nine distinct like values in a month. Now the same actor, same cadence, same month, on the other demo video — the one whose likes never crossed 10,000:
field distinct values unchanged read-to-read span likes 29 / 29 0 of 28 3,213 → 5,908 Twenty-nine reads, twenty-nine different numbers, not one repeat. Same field, same endpoint, same code. The only difference is which side of 10,000 it is on.
At the top end it stops being a precision problem and becomes an absence of data. The 2.4-billion-view video, read on 28 October and again today:
28 Oct 7 Nov change views 2,400,000,000 2,400,000,000 0 shares 1,100,000 1,100,000 0 likes 29,600,000 29,700,000 +100,000 comments 788,400 798,500 +10,100 saves 998,889 1,001,171 +2,282 Views and shares are frozen — not because nothing happened, but because the quantum at that magnitude is 1,000,000 and 1,000 respectively. Ten days of a famous video's traffic rounded away to nothing. The likes and comments columns moved, but only to the next step on their own ladders, and one step at that magnitude is 100,000 — so "+100,000 likes" is consistent with anything from a single like to just under 200,000. The +2,282 saves is the only figure in that table I would put in a chart.
One honest caveat on that exactness. Both columns above came through the Actor and its proxy. Three direct reads from my own server, a few minutes after the 7 November run, all returned collectCount: 999139 and commentCount: 797000 — identical to each other to the digit, and 2,032 saves below what the proxied run had just reported. The precision is real and stable per route; the absolute value is not authoritative across routes. Treat the extra digits as a change detector on a fixed path, not as ground truth.
Counters go down
While I was in there: across every pair of consecutive reads of the same video and field, 23 of 495 comparisons came back lower than the read before — 4.6%. Comments are the worst offender, because comments get deleted: the second Nike video went 154 → 151 comments over a month while views grew by 27,700. Saves dropped three times on the first one. People un-save things.
If you are keeping a high-water mark, use max() across reads, not last(), and do not treat a decrease as a scraping failure.
Fields I would not build on
Being specific about what is empty saves someone a week:
-
repostswas 0 in all 123 rows.statsV2returns"repostCount": "0"even for a 2.4-billion-view video. The field exists; the number does not. -
aiLabelwas null in all 123 rows, andisAiGeneratedfalse in all 123. Absence of a label is not evidence of absence. -
subtitleswas empty in 58 of 123 rows. When present it is usually oneeng-USASR track; nine rows carried a second language (Japanese, Russian, Korean). The URLs are signed and expire — download them in the same session. - A profile input gives you about 10–12 videos, because that is all the public profile page carries. A bigger
maxVideosPerProfilechanges nothing.
Calling it
curl -s -X POST \ "https://api.apify.com/v2/acts/lergassy~tiktok-video-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H 'content-type: application/json' \ -d '{"videoUrls":["https://www.tiktok.com/@zachking/video/6768504823336815877"], "profiles":["duolingo"], "maxVideosPerProfile":5, "minViews":10000}' Rows are $0.001 each with no run fee; videos cut by minViews or publishedAfter are free, and a dead link returns a free error row saying why — "video not found — it is private or removed" for the one bogus id in my corpus. The Actor is TikTok Video Scraper if you want the whole field list.
When it is the wrong tool: if you need exact view counts above 9,999, nobody can sell you those — the rounding happens on TikTok's side of the wire, and an Actor promising five-digit precision there is inventing digits. If you need comment text or a transcript rather than counters, this returns neither.
What I changed in my own head after a month of logs: build the growth chart on saves, keep views for the headline, and never diff two rounded numbers without checking that the difference is bigger than the quantum. 10 ** (len(str(views)) - 4) is not a formatting detail — it is the resolution of your dataset, and it grows with the number.


