Skip to content

BUILD LOG

Apple throttles with a 200 and an empty body

· 5 min read

Every month we read tens of thousands of live App Store reviews to work out what people actually dislike about the apps that are winning. In one run, a category came back with almost no review evidence at all. Not an error, not a timeout. Just apps that appeared to have no recent reviews.

They had reviews. We were being throttled, and the throttle was invisible.

Method card, step 2 of 5: mine the reviews of whatever is charting. 60,000+ live reviews read per run, around 200 per app, pulled from each storefront in its own market.
The stage this bug quietly emptied: ~200 recent reviews per charting leader, per storefront, every run.

The failure mode

Apple's customer-reviews feed is an old, undocumented endpoint. When you request it too often it does not answer with 429, and it does not answer with an error body. It answers 200 OK with a well-formed payload containing no entries.

That is indistinguishable, at the HTTP layer, from an app that genuinely has no reviews in that storefront. Our fetch wrapper checked status codes, backed off on 429, and retried on 5xx. It saw a clean 200 and moved on, every time, faster and faster, because empty responses come back quickly.

The mistake that cost the most time

The genuinely instructive part is not the bug. It is that we nearly closed it as correct behaviour.

Presented with one app returning zero reviews, the reasonable-sounding conclusion was that the app simply had none in that market. The data was internally consistent. What broke the assumption was checking the same app in our own history: it had returned between one and two hundred reviews in five previous runs. Nothing about the app had changed.

Validating suspect data against other suspect data from the same poisoned batch will confirm almost anything. The check that worked was comparing against a period we already knew was clean.

What we changed

Status alone cannot see this failure, so callers now report the shape of what came back, not just whether the request succeeded:

  • An empty first page is recorded as a signal. Only the first page, because a later page is legitimately empty once an app runs out of reviews.
  • It is counted as a streak across apps. One review-less app is normal; three in a row is not a coincidence, it is a throttle.
  • Once the streak trips, the fetcher slows down before the next request rather than after the next failure, and it says so in the logs.
  • The mining stage now aborts and writes nothing if more than 30% of its pulls came back empty. A partial harvest that looks complete is worse than no harvest, because it silently becomes evidence.

The principle underneath

Politeness has to be preventative. By the time a rate limiter tells you to stop, you have already spent your reputation with that host, and on shared infrastructure that reputation is not only yours to spend. So we now pace deliberately, treat silence as a signal, and refuse to publish a run built on thin evidence.

That last rule is not new. Our reports ship all 25 categories or none of them, and this change extends the same idea one layer down: a category ships with real review evidence or it does not ship. You can read a complete run for free, or see exactly how the scoring works.