Blog/Tutorial

How to Scrape Amazon Without Getting Blocked

Jun 18, 2026·11 min read

Amazon is one of the most aggressively protected sites for web scraping. It uses a multi-layer detection system: IP reputation, request fingerprinting, behavioral analysis, and CAPTCHAs. This guide covers the complete strategy for collecting Amazon product data, pricing, and reviews at scale — with Python code you can run today.

How Amazon detects bots

Understanding Amazon's detection system is prerequisite to bypassing it. Amazon uses several signals:

IP reputation: Datacenter ASNs are blocked outright. Amazon maintains a large blocklist of hosting provider IP ranges. Only residential and ISP IPs have a chance.
Request rate: More than ~30–60 requests from a single IP within a short window triggers rate limiting and CAPTCHA challenges, regardless of IP type.
HTTP headers: Missing headers (no Accept-Language, no User-Agent, no Accept-Encoding), unusual header order, or non-browser TLS fingerprints immediately flag automated traffic.
Cookie state: Amazon issues session cookies on first visit. Requests without cookies, or with stale/invalid cookie state, trigger challenges.
Behavioral signals: Mouse movement data (via JavaScript), page load timing, and click patterns. Not a concern for server-side HTML scrapers that don't execute JavaScript.

Step 1: Set up rotating residential proxies

Amazon blocks all datacenter IPs. You need residential proxies. Here's the base configuration:

python
import requests
import time
import random

PROXY_URL = "http://YOUR_USERNAME:[email protected]:8080"

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
}

def fetch_amazon(url: str, retries: int = 3) -> str | None:
    for attempt in range(retries):
        try:
            response = requests.get(
                url,
                headers=HEADERS,
                proxies={"http": PROXY_URL, "https": PROXY_URL},
                timeout=30,
            )
            if response.status_code == 200 and "captcha" not in response.url:
                return response.text
            if response.status_code == 503:
                time.sleep(2 ** attempt + random.uniform(0, 1))
                continue
        except requests.RequestException:
            time.sleep(1)
    return None

Step 2: Parse product data with BeautifulSoup

python
from bs4 import BeautifulSoup
import re

def parse_product(html: str) -> dict:
    soup = BeautifulSoup(html, "lxml")

    def get_text(selector: str) -> str:
        el = soup.select_one(selector)
        return el.get_text(strip=True) if el else ""

    # Title
    title = get_text("#productTitle")

    # Price — Amazon uses multiple possible selectors depending on variant
    price_raw = (
        get_text(".a-price .a-offscreen") or
        get_text("#priceblock_ourprice") or
        get_text("#priceblock_dealprice") or
        get_text(".apexPriceToPay .a-offscreen")
    )
    price = re.sub(r"[^\d.]", "", price_raw)

    # Rating
    rating_raw = get_text(".a-icon-star .a-icon-alt")
    rating_match = re.search(r"([\d.]+) out of", rating_raw)
    rating = rating_match.group(1) if rating_match else ""

    # Review count
    reviews_raw = get_text("#acrCustomerReviewText")
    review_count = re.sub(r"[^\d]", "", reviews_raw)

    # ASIN
    asin_match = re.search(r'/dp/([A-Z0-9]{10})', soup.find("link", {"rel": "canonical"})["href"]) if soup.find("link", {"rel": "canonical"}) else None
    asin = asin_match.group(1) if asin_match else ""

    return {
        "title": title,
        "price": price,
        "rating": rating,
        "review_count": review_count,
        "asin": asin,
    }


# Usage
html = fetch_amazon("https://www.amazon.com/dp/B0CHWRXH8B")
if html:
    product = parse_product(html)
    print(product)

Step 3: Batch scraping with rate limiting

For scraping multiple ASINs, add delays and randomization to mimic human behavior:

python
import asyncio
import httpx
import random
import time
from typing import Optional

PROXY_URL = "http://USERNAME:[email protected]:8080"
BASE_URL = "https://www.amazon.com/dp/{asin}"

async def fetch_asin(client: httpx.AsyncClient, asin: str, semaphore: asyncio.Semaphore) -> dict:
    async with semaphore:
        url = BASE_URL.format(asin=asin)
        try:
            response = await client.get(url, timeout=30)
            if response.status_code == 200 and "captcha" not in str(response.url):
                data = parse_product(response.text)
                data["asin"] = asin
                data["status"] = "ok"
            else:
                data = {"asin": asin, "status": f"blocked_{response.status_code}"}
        except Exception as e:
            data = {"asin": asin, "status": f"error_{type(e).__name__}"}

        # Randomized delay between requests
        await asyncio.sleep(random.uniform(1.5, 4.0))
        return data

async def scrape_asins(asin_list: list[str], concurrency: int = 5) -> list[dict]:
    semaphore = asyncio.Semaphore(concurrency)
    async with httpx.AsyncClient(
        proxy=PROXY_URL,
        headers=HEADERS,
        follow_redirects=True,
    ) as client:
        tasks = [fetch_asin(client, asin, semaphore) for asin in asin_list]
        results = await asyncio.gather(*tasks)
    return list(results)

# Example: scrape 20 ASINs
asins = ["B0CHWRXH8B", "B09B8V1LZ3", "B08J5F3G18"]  # replace with your list
results = asyncio.run(scrape_asins(asins, concurrency=5))
print(f"Scraped {len([r for r in results if r.get('status') == 'ok'])} / {len(asins)} products")

Handling CAPTCHAs

Amazon's CAPTCHA page is distinct — check for it explicitly rather than assuming a 200 response means success:

python
def is_captcha(response: requests.Response) -> bool:
    return (
        "captcha" in response.url or
        "api-services-support" in response.url or
        "Type the characters you see in this image" in response.text or
        "Enter the characters you see below" in response.text
    )

def is_blocked(response: requests.Response) -> bool:
    return response.status_code in (403, 503) or is_captcha(response)

# In your scraping loop:
response = requests.get(url, ...)
if is_blocked(response):
    # The IP got flagged — the rotating gateway will assign a new one next request
    time.sleep(random.uniform(5, 15))
    continue

With a rotating residential proxy, a flagged IP is automatically replaced on the next request — you don't need to manage IP rotation manually. Keep exponential backoff in your retry logic to avoid hammering the target.

Key rules for sustainable Amazon scraping

01Keep concurrency to 3–5 requests per rotating proxy endpoint. Amazon's rate-limiting is per-IP, not per-session.
02Add 1.5–4s random delay between requests. Consistent intervals are a bot signal.
03Rotate User-Agent strings from a realistic browser pool — don't use a single UA string across thousands of requests.
04Use rotating residential proxies, not datacenter or shared proxies. Amazon's IP blocklist is extensive and continuously updated.
05Collect HTML and parse offline. Don't execute JavaScript — Amazon's JS-based signals don't matter for server-side requests.
06Respect Amazon's ToS. Use this for price monitoring, research, and competitive intelligence — not for content redistribution.

Ready to run this scraper?

Get residential proxy credentials in 2 minutes.

42M+ genuine ISP-assigned IPs. Works on Amazon, Google, LinkedIn, and all major anti-bot protected sites. $1/GB, no subscription.

← All posts