App Store Scraper for extracting app details, ratings, reviews, pricing, developer information, screenshots and version history from the Apple App Store. This repo has a free App Store web scraping script you can run right now, and an App Store data API with 3 endpoints returning real structured JSON.
Last updated: 2026-07-16. Working against the Apple App Store as of July 2026, and re-verified whenever Apple changes their response shape.
Every JSON block on this page was captured from the live API on 2026-07-16. Long arrays are trimmed to the first item or two and long strings are cut where marked, and each block states exactly what was cut. Full uncut samples are committed in app_store_scraper_api_data/, so you can diff this page against them. Every code example calls the actual API and is runnable from app_store_scraper_api_codes/.
pip install requests
export CHOCODATA_API_KEY="your_key" # free: 1,000 requests, one-time, no card
python app_store_scraper_api_codes/search.pyThose three lines print the full JSON plus a summary line. Here is the top result, cut to the 10 fields you probably came for (all 36 are in the reference below, verbatim and uncut):
{
"position": 1,
"id": "324684580",
"bundle_id": "com.spotify.client",
"title": "Spotify: Music and Podcasts",
"seller_name": "Spotify",
"formatted_price": "Free",
"rating": 4.77884,
"reviews_count": 40977158,
"version": "9.1.62",
"primary_genre": "Music"
}...multiplied by 8 apps, with 36 fields each:
That is the whole point of this repo. The rest of this page is how it works and what each endpoint returns.
- Free App Store Scraper
- Avoid getting blocked when scraping the App Store
- App Store Scraper API reference
- Track a competitor app's rank and reviews
- Measured latency
- License
Start here, because it works. Apple publishes a public iTunes Search API and a public Lookup API. No key, no account, no vendor, no scraping in the HTML-parsing sense at all: they return JSON directly.
python free_scraper/app_store_free_scraper.py "spotify"
python free_scraper/app_store_free_scraper.py --id 324684580Source: free_scraper/app_store_free_scraper.py. It calls itunes.apple.com/search and itunes.apple.com/lookup directly and emits trackId, trackName, sellerName, primaryGenreName, formattedPrice, currency, averageUserRating, userRatingCount, version, trackViewUrl.
After running the command, your terminal should look something like this:
That is a real run. Apple's endpoint is free, it is fast, it returns 44 fields per app, and nobody is going to send you a captcha.
The free route covers reviews too: itunes.apple.com/{country}/rss/customerreviews/page=1/id={id}/sortby=mostrecent/xml returns 50 reviews as an Atom feed, no key. We hit it 60 times at ~1.8 requests/second and got 60 populated feeds back, with no throttling.
The free route has one hard edge, and it is not where you would expect.
You will not get blocked. There is no bot check, no captcha, no "Robot or human?" interstitial, and no amount of header tuning is relevant. Anyone telling you the App Store needs a headless browser and a fingerprint story is describing a different website.
What Apple actually enforces is a rate limit, and it is undocumented. There is no published quota for these endpoints and no status page, so the only way to find the edge is to walk into it. Here is what we measured on 2026-07-16 from one residential IP, using the free scraper above:
| What we measured | Value |
|---|---|
--slow 30 (sequential, ~1/s) |
29 x 200, 1 x 403. Essentially fine. |
--burst 120 (concurrent, ~11/s) |
30 x 200, 30 x 429, 60 x 403. 90 of 120 calls lost. |
| First throttle arrived after | 11 of 120 answers had come back |
| The 429 | 74 byte body, and it does send Retry-After: 28. |
| The 403 | 0 bytes, no message, no error code, and no Retry-After at all. |
| How long the 403 lasted | 48 minutes, polling once a minute |
| Recovery | not clean: 200s and 403s interleave for a while before it settles |
/lookup + the RSS feeds at the same moment |
200. The limit is per endpoint. |
Apple throttles you two different ways and only one of them is polite. The 429 behaves: 74 bytes, Retry-After: 28, wait 28 seconds and carry on. The 403 does not: zero bytes, no header, no hint, and in our measurement it sat there for 48 minutes. The same burst produced both, so a client that only handles the documented-looking case handles half the problem.
Both rows of that table are reproducible with the two commands the script gives you. It prints the body size and the Retry-After of whatever throttle you actually hit, rather than assuming ours:
python free_scraper/app_store_free_scraper.py --slow 30 # the control group
python free_scraper/app_store_free_scraper.py --burst 120 # the ceilingFair warning: running --burst costs you that IP's access to /search for the best part of an hour. That is the finding, not a side effect.
That is the shape of the problem, and it is a different problem from the one most scraper repos describe:
| What bites you | Why | What it costs you |
|---|---|---|
| The 403 is empty and silent | Zero-byte body, no error code, no Retry-After. Your JSON parser gets nothing and your code logs "0 results", not "throttled". |
The expensive failure is not a crash. It is a week of empty data that looked like "no matches". |
| It persists, and far longer than you would guess | Once tripped, /search answered 403 for 48 minutes. Even after the first 200 came back, the next call three seconds later was a 403 again. |
A backoff tuned in seconds never clears it, and a naive "it worked once, we are back" check will re-trip immediately. |
| It is per endpoint | While /search was locked out, /lookup, the reviews feed and the charts feed all returned 200 from the same IP in the same second. |
A health check pointed at the wrong endpoint reports green while your search pipeline is dark. |
| Per-storefront data multiplies your call count | rating, reviews_count, formatted_price and even title are per-country values (see Product). One app across 20 storefronts is 20 calls, not 1. |
The matrix is exactly what trips the limit, and the matrix is usually the reason you came. |
| Apple documents none of it | No published quota for these endpoints, no status page, and the throttle you get is not one consistent thing: we saw both 429s and 403s in the same burst. | You discover your limit by hitting it in production. |
The limit is not a wall, it is a speed limit with no posted number and no warning light. Sequential polling stays under it. A competitive set times a storefront matrix on a schedule finds it.
The Chocodata App Store Scraper API is the managed option, and the one this repo is built around. Three endpoints for App Store data extraction at scale (search listings, app product pages and customer reviews), a ~99% success rate, no rate-limit backoff to write, and no IP supply to buy. Free for the first 1,000 requests.
This is Apple's public JSON, normalized. trackId becomes id, sellerName becomes seller_name, userRatingCount becomes reviews_count; the review feed's Atom XML becomes a JSON array. There is no secret door into the App Store, and anyone selling you that story is selling the same public endpoints.
What the API adds is the part between you and those endpoints: the rate limit absorbed so a 120-app storefront matrix does not cost you an hour of 403s, 50 concurrent requests instead of ~20, one consistent snake_case shape across all three surfaces, and the XML feed parsed.
The App Store Scraper API reference starts here: three endpoints, each with its parameters, a runnable call, and the response it returns.
curl "https://api.chocodata.com/api/v1/appstore/search?api_key=YOUR_KEY&term=spotify"import requests
r = requests.get(
"https://api.chocodata.com/api/v1/appstore/search",
params={"api_key": "YOUR_KEY", "term": "spotify"},
timeout=90,
)
top = r.json()["results"][0]
print(top["title"], top["rating"], top["reviews_count"])
# Spotify: Music and Podcasts 4.77884 40977158After running the command, your terminal should look something like this:
Get a key at chocodata.com (1,000 requests, one-time, no card).
Pass api_key as a query parameter on every request. That is the whole auth model.
Accepted by every endpoint below:
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
api_key |
string | yes | - | Your Chocodata API key. |
country |
string (ISO-2) | no | us |
Storefront. The App Store is a different catalogue in every country: ratings, review counts, prices and app titles all change with it. |
Each request costs 5 credits (= 1 request). Responses are billed only on success (2xx).
Real captured error bodies, not paraphrases. Nothing below is billed: you are only charged on a 2xx.
| Status | error code |
Meaning | Billed | What to do |
|---|---|---|---|---|
400 |
invalid_params |
A required param is missing or out of range. The body's issues[] names the exact path. |
no | Fix the query string. |
401 |
INVALID_API_KEY |
Key missing, unrecognised, or revoked. | no | Check api_key. Get one at chocodata.com. |
402 |
INSUFFICIENT_CREDITS |
Balance exhausted. | no | Top up ($0.90 / 1,000 requests, never expires) or upgrade. |
429 |
RATE_LIMITED |
Over your plan's concurrency. | no | Back off and retry; see Rate limits. |
502 |
extraction_failed |
Apple returned nothing usable for this request. A nonexistent app id lands here, not on a 404 (see below). | no | Check the id, then retry. |
There is no 404 on this API. Apple answers a dead app id with HTTP 200 and an empty result set, so a bad id surfaces as a 502, not a 404. If you are branching on status codes to tell "this app does not exist" from "try again later", that distinction is not available: check the id yourself first.
Two response shapes exist: auth/billing errors nest under error.code (uppercase), while validation errors are flat with a lowercase error string plus issues. Both are shown below.
The scripts in this repo map every documented status onto an actionable message, so a typo'd key does not hand you a stack trace:
A bad key, verbatim:
curl "https://api.chocodata.com/api/v1/appstore/search?api_key=totally_invalid_key_123&term=spotify"{"error":{"code":"INVALID_API_KEY","message":"Api key not recognised."}}A param out of range, verbatim. Note it names the field, the bound, and the path:
curl "https://api.chocodata.com/api/v1/appstore/reviews?api_key=YOUR_KEY&id=324684580&page=11"{"error":"invalid_params","issues":[{"code":"too_big","maximum":10,"type":"number","inclusive":true,"exact":false,"message":"Number must be less than or equal to 10","path":["page"]}]}There is no per-minute request cap. The limit is concurrency: how many requests you may have in flight at once.
| Plan | Concurrent requests |
|---|---|
| Free | 10 |
| Vibe | 30 |
| Pro | 50 |
| Custom | 100 to 500+ |
Exceed it and you get 429, not a queue. Every endpoint is a synchronous GET: there is no webhook, callback, or async job to poll. The examples use timeout=90.
Sizing: at Pro (50 concurrent) and a ~3.4s median search, one worker pool sustains roughly 50 / 3.4 = 15 requests/second. Size off the p90 rather than the median if you want that to hold (see Measured latency). Fan out with a thread pool:
from concurrent.futures import ThreadPoolExecutor
import requests
def one(term):
r = requests.get("https://api.chocodata.com/api/v1/appstore/search",
params={"api_key": KEY, "term": term}, timeout=90)
return term, (r.json().get("results", []) if r.ok else [])
terms = ["spotify", "music streaming", "podcast player", "audiobooks", "radio"]
with ThreadPoolExecutor(max_workers=10) as pool: # <= your plan's concurrency
for term, results in pool.map(one, terms):
print(term, len(results))Ranked App Store search results for a keyword, with the full app record for every hit: pricing, rating, review count, developer, genre, version and screenshots.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
term |
string | yes | - | Search keywords (app name or keyword). |
country |
string (ISO-2) | no | us |
Storefront to search. |
limit |
int (1-200) | no | 10 |
Upper bound on results. Apple returns what it matched, reliably fewer than you ask for. |
curl "https://api.chocodata.com/api/v1/appstore/search?api_key=YOUR_KEY&term=spotify"Real response. results is cut to 1 of 8; the result object itself is complete, all 36 fields verbatim. Inside it, screenshot_urls is cut to 1 of 8, ipad_screenshot_urls to 1 of 6, supported_devices to 2 of 128, languages to 3 of 61, advisories to 1 of 6, and description / release_notes are truncated where marked (full sample):
{
"query": "spotify",
"page": 1,
"total_results": 8,
"results": [
{
"position": 1,
"id": "324684580",
"track_id": "324684580",
"bundle_id": "com.spotify.client",
"title": "Spotify: Music and Podcasts",
"seller_name": "Spotify",
"artist_id": "324684583",
"primary_genre": "Music",
"genres": ["Music", "Entertainment"],
"price": 0,
"formatted_price": "Free",
"currency": "USD",
"rating": 4.77884,
"rating_current_version": 4.77884,
"reviews_count": 40977158,
"reviews_count_current_version": 40977158,
"content_advisory_rating": "12+",
"version": "9.1.62",
"current_version_release_date": "2026-07-07T15:28:00Z",
"release_date": "2011-07-14T11:22:37Z",
"release_notes": "We’re always making changes and improvements to Spotify. To make...",
"file_size_bytes": 283252736,
"minimum_os_version": "16.1",
"description": "With the Spotify app, you can explore an extensive library of music and podcasts for free. Curat...",
"artwork_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/40/04/4e/40044e28-ff9b-e382-fc03-3425835acf16/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-85-220.png/512x512bb.jpg",
"thumbnail": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/40/04/4e/40044e28-ff9b-e382-fc03-3425835acf16/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-85-220.png/512x512bb.jpg",
"screenshot_urls": [
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/f3/ce/05/f3ce0547-690a-b355-b1dd-6b6e91808279/IOS_-_5.5_-_S01_-_EN_-_CA_U005bEnglish__U0028Canada_U0029_U005d.png/392x696bb.png"
],
"ipad_screenshot_urls": [
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/31/15/15/31151525-510a-a212-ea51-e023e7b6351f/IOS_-_Ipad_6th_-_S01_-_EN_-_CA_U005bEnglish__U0028Canada_U0029_U005d.png/552x414bb.png"
],
"supported_devices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir"],
"languages": ["AF", "AM", "AR"],
"advisories": ["Infrequent/Mild Sexual Content and Nudity"],
"is_game_center_enabled": false,
"kind": "software",
"url": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580?uo=4",
"seller_url": "https://www.spotify.com/",
"artist_view_url": "https://apps.apple.com/us/developer/spotify/id324684583?uo=4"
}
]
}The important thing about this endpoint is that it already returns the complete app record, not a thin list row. Every field the Product endpoint gives you is here too, for every hit. If you are searching, you usually do not need a follow-up product call at all.
position is the order Apple's Search API returned for this term, which is a relevance ranking. Treat it as exactly that: Apple does not document how it relates to the order a human sees in the App Store app, and we have not verified that they match. Do not sell it to anyone as their App Store search rank.
limit is an upper bound. Apple returns what it matched, and it is reliably fewer than you asked for: the default limit=10 returned 8 for this term (that is the sample above), limit=20 returned 17, limit=50 returned 42, and limit=200 returned 175. There is no page param and no pagination, so 200 is the maximum for a term, and total_results is the number of rows we returned, not a catalogue-wide count.
Runnable: app_store_scraper_api_codes/search.py
The full App Store record for one app, looked up by numeric id or by bundle id.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | one of id/bundleId |
- | Numeric App Store id. Accepts a bare id (324684580), an id324684580 token, or a full apps.apple.com/... URL. |
bundleId |
string | one of id/bundleId |
- | Reverse-DNS bundle id (e.g. com.spotify.client). |
country |
string (ISO-2) | no | us |
Storefront to look the app up in. |
curl "https://api.chocodata.com/api/v1/appstore/product?api_key=YOUR_KEY&id=324684580"
curl "https://api.chocodata.com/api/v1/appstore/product?api_key=YOUR_KEY&bundleId=com.spotify.client"Both of those return the same app. bundleId is the useful one if you already have an app's identifier from a mobile SDK, an MDM inventory, or your own build config, and never want to guess a numeric id.
Real response. All 35 fields verbatim; screenshot_urls cut to 1 of 8, ipad_screenshot_urls to 1 of 6, supported_devices to 2 of 128, languages to 3 of 61, advisories to 1 of 6, description / release_notes truncated where marked (full sample):
{
"id": "324684580",
"track_id": "324684580",
"bundle_id": "com.spotify.client",
"title": "Spotify: Music and Podcasts",
"seller_name": "Spotify",
"artist_id": "324684583",
"primary_genre": "Music",
"genres": ["Music", "Entertainment"],
"price": 0,
"formatted_price": "Free",
"currency": "USD",
"rating": 4.77888,
"rating_current_version": 4.77888,
"reviews_count": 40967626,
"reviews_count_current_version": 40967626,
"content_advisory_rating": "12+",
"version": "9.1.62",
"current_version_release_date": "2026-07-07T15:28:00Z",
"release_date": "2011-07-14T11:22:37Z",
"release_notes": "We’re always making changes and improvements to Spotify. To ...",
"file_size_bytes": 283252736,
"minimum_os_version": "16.1",
"description": "With the Spotify app, you can explore an extensive library of music and podcasts for fre...",
"artwork_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/40/04/4e/40044e28-ff9b-e382-fc03-3425835acf16/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-85-220.png/512x512bb.jpg",
"thumbnail": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/40/04/4e/40044e28-ff9b-e382-fc03-3425835acf16/AppIcon-0-0-1x_U007epad-0-1-0-0-sRGB-85-220.png/512x512bb.jpg",
"screenshot_urls": [
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/f3/ce/05/f3ce0547-690a-b355-b1dd-6b6e91808279/IOS_-_5.5_-_S01_-_EN_-_CA_U005bEnglish__U0028Canada_U0029_U005d.png/392x696bb.png"
],
"ipad_screenshot_urls": [
"https://is1-ssl.mzstatic.com/image/thumb/PurpleSource211/v4/31/15/15/31151525-510a-a212-ea51-e023e7b6351f/IOS_-_Ipad_6th_-_S01_-_EN_-_CA_U005bEnglish__U0028Canada_U0029_U005d.png/552x414bb.png"
],
"supported_devices": ["iPhone5s-iPhone5s", "iPadAir-iPadAir"],
"languages": ["AF", "AM", "AR"],
"advisories": ["Infrequent/Mild Sexual Content and Nudity"],
"is_game_center_enabled": false,
"kind": "software",
"url": "https://apps.apple.com/us/app/spotify-music-and-podcasts/id324684580?uo=4",
"seller_url": "https://www.spotify.com/",
"artist_view_url": "https://apps.apple.com/us/developer/spotify/id324684583?uo=4"
}The review count wobbles. Compare the block above with the search block: same app, same storefront, same day. Search says reviews_count: 40977158, product says 40967626. A gap of 9,532 ratings.
The tempting explanation is that one surface is simply fresher than the other, and that is what we assumed until we measured it. Then we called /appstore/product ten times in a row and got 40967626 six times and 40977158 four times. Same request, same minute, two different answers. So it is not "search is stale and product is fresh": Apple is serving more than one generation of this counter at the same time, and which one you get is not something you control.
We have not reverse-engineered why. What we can tell you is the shape of it, measured: /appstore/search returned one stable value across 6 calls, /appstore/product returned two distinct values across 10, and the spread was 9,532 ratings, about 0.02% of the total.
We return whatever Apple hands us, verbatim, rather than smoothing it. If you are diffing review counts to detect growth, put your threshold above the wobble or you will log growth that never happened. (It is also why aso_monitor.py reads the whole competitive set from one search call rather than one product call per app.)
country changes more than the price. The same app id in four storefronts, from product.py:
| Storefront | title |
formatted_price |
rating |
reviews_count |
|---|---|---|---|---|
us |
Spotify: Music and Podcasts | Free | 4.78 | 40,967,626 |
gb |
Spotify: Music and Podcasts | Free | 4.75 | 6,773,251 |
de |
Spotify Musik und Podcasts | Gratis | 4.71 | 5,585,565 |
jp |
Spotify: 音楽とポッドキャスト | 無料 | 4.59 | 4,920,173 |
The rating is not a global number, it is a storefront number, and so is the review count and the title. "Spotify is rated 4.78" is a US statement. If you are benchmarking an app against competitors, pin the storefront or you are comparing different populations.
version, release_notes and current_version_release_date describe the current release only. Apple does not expose previous versions on this surface, so a release timeline is something you build by polling, which is what aso_monitor.py does.
Runnable: app_store_scraper_api_codes/product.py
Customer reviews for an app, 50 per page, from Apple's per-storefront review feed.
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | yes | - | Numeric App Store id. |
country |
string (ISO-2) | no | us |
Storefront. Reviews are per storefront: US reviews and JP reviews are different sets. |
page |
int (1-10) | no | 1 |
Review page, ~50 reviews each. |
sort |
mostRecent | mostHelpful |
no | mostRecent |
Sort order. |
curl "https://api.chocodata.com/api/v1/appstore/reviews?api_key=YOUR_KEY&id=324684580"Real response. reviews cut to 1 of 50; the review object is complete, all 10 fields verbatim (full sample):
{
"id": "324684580",
"country": "us",
"page": 1,
"sort": "mostRecent",
"total_results": 50,
"reviews": [
{
"id": "14307057398",
"author": "Dough_not",
"author_url": "https://itunes.apple.com/us/reviews/id1679674957",
"rating": 5,
"title": "BEST MUSIC AND APP",
"body": "I LOVE this app it has what songs I want even if I don’t know the name of it I love it !!",
"version": "9.1.62",
"date": "2026-07-15T10:46:03-07:00",
"vote_sum": 0,
"vote_count": 0
}
]
}version on a review is the underrated field: it tells you which release the reviewer was actually complaining about, which is how you tie a rating drop to a ship date.
total_results is the count on this page, not the app's lifetime review count. The lifetime number is reviews_count on the product/search record: roughly 40.97 million for this app, give or take the wobble.
vote_sum and vote_count are 0 on nearly every mostRecent review, because nobody has voted on a review from yesterday yet. They populate on mostHelpful: the top mostHelpful review we pulled had vote_sum: 62 of vote_count: 96. Both sorts return 50 real reviews and the two sets did not overlap at all, so they are genuinely different queries, not the same feed reordered.
page is capped at 10 by Apple's feed, and page=11 is rejected with a 400. At ~50 per page, 500 reviews is the maximum per app, per storefront, per sort, and that cap is Apple's. For a high-volume app, 500 covers a short window: on the day we captured this, page 1's newest review was 2026-07-15T10:46 and page 10's newest was 2026-07-14T11:08, so all ten pages together spanned barely 24 hours. That is the number to schedule against. Reading the latest complaints and tying them to a version is what this endpoint is for, and polling it daily builds the history you keep.
There is no rating distribution on this endpoint. Apple's feed does not carry one, so we do not return one. You can count the stars of the 50 reviews on a page, and that is a sample, not a distribution.
Runnable: app_store_scraper_api_codes/reviews.py
Watching a competitive set is the main reason people scrape the App Store, so that use case is in the repo end to end rather than as a snippet. aso_monitor.py polls one search, stores every observation as a local dataset in SQLite (export it to CSV with one sqlite3 command), and prints what moved since the last run:
python app_store_scraper_api_codes/aso_monitor.py "music streaming"
# 14 apps this run | 0 change(s) | 14 apps tracked in app_store_rankings.db
# No changes yet. Run it again in an hour, or schedule it (cron / GitHub Actions).That is a real first run: the first pass has nothing to compare against, so it seeds the database and says so. Each subsequent change prints as a diff line, one of RATING, REVIEWS, VERSION, PRICE or RANK, in the format <what> <title> <before> -> <after>, which is what aso_monitor.py emits.
How often to run it, measured rather than guessed: we ran it twice, 45 minutes apart, and it reported 0 changes both times. That is not the script failing, it is Apple's search index being cached: the rating and reviews_count on the search surface returned byte-identical values across the whole session. Schedule this hourly or daily. Polling it every five minutes will burn requests to tell you nothing.
The reason this is one API call and not fifteen: search already returns the full record for every hit, so rating, reviews_count, version and position for the whole competitive set arrive together. One request (5 credits) per run, per term, whatever the size of the set. 1,000 free requests covers roughly a month of hourly checks on one term.
The VERSION line is the one worth wiring an alert to. A competitor's version bump is a dated event you can correlate against their rating moving, and it is the closest thing to a release timeline the App Store will give you.
Real end-to-end wall-clock, measured from a laptop against the live API on 2026-07-16. This includes the upstream fetch and the parse:
| Endpoint | Median | p90 | Range | n |
|---|---|---|---|---|
/appstore/search |
3.4s | 7.3s | 1.4 to 9.6s | 15 |
/appstore/product |
1.8s | 6.2s | 1.1 to 9.1s | 15 |
/appstore/reviews |
1.8s | 3.8s | 1.0 to 9.3s | 15 |
Read the ranges, not the medians. Every endpoint here has a tail to roughly 9s, including the ones with a sub-2s median, so size your timeouts off the p90 and not off the happy path. That is why the examples use timeout=90 rather than something that looks like the median. Small sample (n=15); reproduce it yourself with the scripts here.
MIT. See LICENSE.








