Track Amazon BSR (Best Sellers Rank) Over Time
You are researching products to launch — or watching one you already sell. Best Sellers Rank (BSR) is Amazon's single best public signal of how a product is actually moving relative to its category. A drop in BSR is an early-warning that demand is shifting; a steady BSR over six months says the product is durable.
Amazon does not publish BSR history. You build it yourself by scraping the rank field daily and persisting it. This guide covers the DIY scrape, where the data lives in the HTML, and how to schedule the pipeline reliably.
Why BSR Tracking Is Different From Price Tracking
Three things make BSR awkward to scrape compared to price:
The number is buried in the page. BSR lives in a specific section of the product page that Amazon changes the location of routinely. Sometimes inside a <table id="productDetails_detailBullets_sections1">, sometimes in a <div id="detailBulletsWrapper_feature_div">, sometimes under "Product information" on mobile pages. Your selector needs to fall back across these locations.
Sub-category BSR matters more than parent. A product might have BSR #142 in Electronics (the parent, with a huge pool) and BSR #3 in "Wireless Earbud Headphones" (a sub-category that shoppers actually browse). For research, the sub-category number is the useful one.
It is a snapshot, not a metric. Unlike price (a single field), BSR is a (rank, category) tuple. A product can be ranked in 3-5 categories simultaneously. Your data model needs to handle a list of rank entries per scrape, not a scalar.
Update timing is uncertain. Amazon refreshes BSR at unspecified intervals. The number you scrape at 9am is up to a few hours stale. Daily granularity is the practical floor.
The DIY Approach
import re
import requests
from bs4 import BeautifulSoup
HEADERS = {
"User-Agent": "Mozilla/5.0 ... Chrome/127.0.0.0 ...",
"Accept-Language": "en-US,en;q=0.9",
}
BSR_RE = re.compile(r"#([\d,]+)\s+in\s+([A-Za-z0-9 &']+)")
def parse_bsr(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
text = soup.get_text(" ", strip=True)
out = []
for match in BSR_RE.finditer(text):
rank = int(match.group(1).replace(",", ""))
category = match.group(2).strip()
out.append({"rank": rank, "category": category})
return out
def scrape_bsr(asin: str) -> list[dict]:
url = f"https://www.amazon.com/dp/{asin}"
r = requests.get(url, headers=HEADERS, timeout=15)
if r.status_code != 200 or "validateCaptcha" in r.url:
return []
return parse_bsr(r.text)
if __name__ == "__main__":
for entry in scrape_bsr("B09V3KXJPB"):
print(entry)
# Example output:
# {"rank": 142, "category": "Electronics"}
# {"rank": 3, "category": "Wireless Earbud Headphones"}
The regex above catches the common "#142 in Electronics" pattern. It is deliberately permissive — Amazon's category names contain spaces, ampersands, and apostrophes.
Real Limitations
- Selector instability. The BSR HTML location changes every few months. The text-regex approach above is more durable than a CSS selector, but still requires periodic validation.
- CAPTCHA threshold same as product scraping. ~5-30 requests per residential IP before challenges.
- Mobile vs desktop pages differ. Your User-Agent must look like a desktop browser to get the full product-details section.
- Sub-category granularity varies. Some products show five sub-category ranks; some show only the parent. You cannot force Amazon to expose all of them.
A failure mode that costs you a week of data:
# Yesterday's parser worked fine.
# Today Amazon shipped a new desktop layout where BSR lives inside a new container.
# The regex still finds "#142 in Electronics" in the *header* (a related-products carousel)
# instead of the actual product's BSR. You get garbage data with no error.
Always sanity-check: BSR for a known product should change slowly. If yesterday's BSR was 142 and today's is 8,901, the parser is probably reading a different field.
Scaling Beyond a One-Product Script
For a research pipeline tracking dozens or hundreds of ASINs:
Persist all category ranks per scrape. Do not pick "the" rank — store all of them. A bsr_history table with (asin, observed_at, category, rank) is right.
Run daily, not hourly. BSR updates are noisier than price. Hourly granularity adds noise without signal for most use cases.
Compute relative changes, not absolute. A move from rank 3 to rank 8 in a sub-category is a 167% rise. The absolute number alone does not tell you the move size.
Normalize against category size. Rank 142 in Electronics is excellent. Rank 142 in Books is invisible. Track a category_pool_estimate alongside the rank if you compare across categories.
For the scraping layer, since BSR is not a built-in monitor metric in any platform, the cleanest pattern is to schedule a periodic full-page scrape and parse BSR from the result yourself. The LogPose smart endpoint returns the structured product data including the raw fields BSR lives in:
import os
import time
import json
import requests
from datetime import datetime, timezone
from pathlib import Path
API_KEY = os.environ["LOGPOSE_API_KEY"]
BASE = "https://api.logposervices.com/api/v1"
HEADERS = {"X-API-Key": API_KEY}
def scrape(url: str) -> dict:
submit = requests.get(
f"{BASE}/ecommerce/amazon/smart",
params={"url": url, "pages": 1},
headers=HEADERS, timeout=30,
).json()
job_id = submit["job_id"]
while True:
s = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=15).json()
if s["status"] in ("completed", "failed"):
break
time.sleep(2)
return requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=15).json()
asins = ["B09V3KXJPB", "B0BN93GFMN", "B0BTFKR638"]
out_path = Path(f"bsr_{datetime.now(timezone.utc).date()}.jsonl")
with out_path.open("a") as f:
for asin in asins:
data = scrape(f"https://www.amazon.com/dp/{asin}")
ranks = data.get("best_sellers_rank") or data.get("bsr") or []
for entry in ranks:
row = {
"asin": asin,
"observed_at": datetime.now(timezone.utc).isoformat(),
**entry,
}
f.write(json.dumps(row) + "\n")
Schedule this script daily via cron, systemd timer, or GitHub Actions. Append to a JSONL file or pipe into Postgres. Plot rank-over-time per (asin, category) in the tool of your choice.
The LogPose monitor endpoint currently tracks price, rating, and availability as scalar metrics on Amazon. BSR is multi-valued (per-category) so it lives outside the scalar-metric model — schedule your own scrape until that changes.
Common Mistakes
- Treating BSR as a single number. It is a list of
(category, rank)pairs. Store the list. - Comparing BSR across categories. Rank 142 in Electronics and rank 142 in Books are completely different signals.
- Storing only the latest value. The whole point is the trend. Append, do not update.
- Scraping hourly. BSR updates lag and noise dominates; daily is sharper.
- Trusting a sudden 10× rank change. Verify the parser is still reading the right HTML region before alerting on dramatic moves.
The Landscape
For Amazon-specific BSR tracking:
- Keepa — strongest BSR history product on the market; charts BSR alongside price for any ASIN.
- JungleScout / Helium 10 — sales-estimate tools that derive volume from BSR; useful for product research, not raw rank tracking.
- AMZScout — similar product research; lighter on history, lower price point.
- DIY + Bright Data — full control, you own the data and the schema.
- LogPose —
smartendpoint returns product details with BSR in the raw fields; useful when BSR is one of several signals you track across multiple sites.
If BSR-only is your need, Keepa is usually the simplest. If you are combining BSR with reviews, prices, and competitor analysis across platforms, a general-purpose API plus your own storage gives you flexibility Keepa does not.
Get Started
- Sign up at logposervices.com, generate an API key.
- Build the daily-scrape script above and put it on cron.
- After a week of data, plot rank-over-time per ASIN in your sub-category of interest.
Related: scrape Amazon prices in Python, bulk ASIN extraction, Amazon reviews API.
External: Amazon BSR explainer (Helium 10), Python requests.