We use cookies to enhance user experience, personalize content, and analyze traffic. Cookie Policy

← Back to all articles

Price Monitoring: Build a Reliable Scraping Pipeline

Build a price monitoring pipeline that collects accurate product prices, detects meaningful changes, and sends useful alerts without wasting requests.

by Unknown Proxies

15 min read

July 1, 2026

Updated October 17, 2026

Price Monitoring: Build a Reliable Scraping Pipeline

Price monitoring is the repeated collection and comparison of product prices so you can detect real changes over time. To build it, keep a product catalog, fetch each page on a schedule, extract price and currency, validate the result, store every observation, and alert only when the new price differs from the last valid one for the same offer.

Start with an official API, licensed feed, or your own merchant export. If those do not cover a legitimate need and public-page collection is permitted, use a small, paced scraper with a defined product list. Review applicable terms, robots.txt, and legal requirements before collecting data.

This guide covers the data model, a working Python example, the collection schedule, change detection, proxy decisions, and the operational checks that turn page observations into useful alerts. If the collection layer itself is still undecided, use the ecommerce scraping tool guide to choose between APIs, HTTP clients, crawler frameworks, and browser automation.

If the source data comes from competitor product pages, start with the competitor price scraping guide to define scope, offer matching, and responsible collection before those observations enter this pipeline.

If the monitored offer is a hotel stay, use the hotel price scraping guide to keep dates, occupancy, room identity, booking terms, taxes, and fees attached to each quote before it reaches this pipeline.

Price monitoring pipeline: the core workflow

Treat each price check as a data observation, not as an overwrite of the last value. Keeping the observation history makes changes auditable and lets you repair alert logic without fetching the page again.

  1. Maintain a canonical catalog of products and offers to monitor.
  2. Schedule checks according to business value and expected change frequency.
  3. Apply policy, scope, cache, and rate checks before fetching.
  4. Retrieve the permitted page or API response.
  5. Classify the response before extracting data.
  6. Extract product identity, offer, price, currency, shipping, and availability.
  7. Normalize and validate the observation.
  8. Compare it with the last valid comparable observation.
  9. Store the observation, then send a deduplicated alert if a rule matches.

Price monitoring pipeline from a product catalog through scheduling, collection, validation, comparison, storage, and alerts

The validation step is the boundary between collection and monitoring. A request can succeed while the observation is wrong. For example, a consent page may return HTTP 200, or the parser may capture a crossed-out list price instead of the current offer.

Define what "price" means first

One product page can show several valid numbers:

Choose one price definition for each monitor. If the goal is competitor shelf-price tracking, you might capture the current generally available offer and keep coupons in a separate field. If the goal is customer checkout cost, the monitor may need shipping and other mandatory charges as well.

Do not silently substitute one type for another. A missing current price should become a classified null observation, not the list price. Store the raw display text alongside the normalized value so an operator can audit unusual changes.

A practical observation schema looks like this:

{
  "product_id": "catalog-1842",
  "source": "shop.example",
  "source_product_id": "SKU-492",
  "offer_type": "standard",
  "seller": "Example Retailer",
  "amount": "49.95",
  "currency": "USD",
  "shipping_amount": "0.00",
  "availability": "in_stock",
  "observed_at": "2026-07-01T12:00:00Z",
  "parser_version": "shop-example-v3",
  "source_url": "https://shop.example/products/SKU-492"
}

Use a decimal type for money in application code and storage. Binary floating-point arithmetic can introduce rounding errors that make threshold comparisons unreliable.

Build a stable product and offer identity

Price history is useful only when every observation refers to the same thing. Titles and URLs alone are weak identifiers: titles change, tracking parameters create duplicate URLs, and one page may contain several sizes or sellers.

Create an internal product ID, then map each source to a stable source ID such as a SKU, GTIN, model number, or marketplace listing ID. Add variant attributes such as size, color, quantity, condition, seller, and fulfillment method when they affect the offer.

Before enqueueing a page:

Keep product matching separate from price extraction. If a monitor cannot prove that the page still represents the expected product and variant, quarantine the observation instead of adding it to the price series.

Choose the least complex data source

Use the most stable authorized source available:

Source Best fit Main tradeoff
Official API or licensed feed Structured commercial integrations Access, quotas, field coverage, and license terms
Merchant or seller export Monitoring your own catalog Does not cover external sellers
Server-rendered public HTML Small permitted page sets Markup and offer placement can change
Browser-rendered page Permitted fields that require JavaScript More compute, bandwidth, and page-state complexity

Start with a normal HTTP client if the required fields exist in the initial response. A browser should be an evidence-based choice, not the default. It loads more resources, costs more to operate, and creates additional failure modes around cookies, rendering, and sessions.

For a concrete HTTP implementation, the Python Requests proxy guide shows connection setup and bounded retries. If a permitted field genuinely depends on rendering, use an isolated context as described in the Playwright proxy guide.

The Robots Exclusion Protocol defines how crawlers retrieve and interpret robots.txt. Robots directives are only one input: they do not replace permission, site terms, privacy review, or applicable law.

Schedule checks without wasting requests

More frequent checks do not automatically produce better data. Set the interval according to how often a product changes, how quickly the business needs to react, and what request volume the source permits.

Use a tiered schedule:

Add deterministic jitter so thousands of products do not fire at the top of the hour. Cache responses where appropriate, honor validators such as ETag and Last-Modified, and avoid fetching unchanged assets that are not needed for extraction.

Estimate load before deployment. For 12,000 products checked every six hours, the baseline is 48,000 requests per day, or roughly 0.56 requests per second averaged across the full day. The delay calculator helps translate task counts and delays into request pacing, but source-specific limits still control the final schedule.

Extract and normalize comparable prices

Prefer structured fields over brittle presentation selectors when they represent the intended offer. Many product pages embed schema.org Product data as JSON-LD, with offers.price, offers.priceCurrency, and offers.availability. Google documents the same fields for product structured data (page last updated September 2026). Read that block first and fall back to a CSS selector only when it is missing or does not match the visible offer.

Use a source-specific adapter rather than one universal selector set.

Each adapter should return either a typed observation or a classified failure:

from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class PriceObservation:
    product_id: str
    offer_type: str
    amount: Decimal
    currency: str
    available: bool

def comparable_total(observation: PriceObservation, shipping: Decimal) -> Decimal:
    return observation.amount + shipping

Normalize:

Do not compare €39,99 with $39.99 as though they were the same value. Either keep each market and currency in a separate series or convert using a recorded exchange rate and timestamp. If you convert, retain both the source amount and the converted amount.

A minimal working example in Python

This script fetches product pages with httpx, reads the JSON-LD offer with parsel, appends every observation to SQLite, and prints a line when the price moves by more than 5% against the last valid observation. It needs Python 3.10 or newer and pip install httpx parsel. Replace the example URL with a page you are allowed to monitor.

import json
import random
import sqlite3
import time
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

import httpx
from parsel import Selector

db = sqlite3.connect("prices.db")
db.execute(
    """CREATE TABLE IF NOT EXISTS observations (
        product_id TEXT NOT NULL,
        observed_at TEXT NOT NULL,
        status TEXT NOT NULL,
        amount TEXT,
        currency TEXT
    )"""
)


def find_offer(html: str) -> dict | None:
    for raw in Selector(text=html).css('script[type="application/ld+json"]::text').getall():
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            continue
        nodes = data if isinstance(data, list) else [data]
        for node in list(nodes):
            if isinstance(node, dict) and isinstance(node.get("@graph"), list):
                nodes.extend(node["@graph"])
        for node in nodes:
            if isinstance(node, dict) and node.get("@type") == "Product":
                offers = node.get("offers")
                offer = offers[0] if isinstance(offers, list) and offers else offers
                if isinstance(offer, dict) and "price" in offer:
                    return offer
    return None


def record(product_id: str, status: str, amount: str | None = None, currency: str | None = None) -> None:
    db.execute(
        "INSERT INTO observations VALUES (?, ?, ?, ?, ?)",
        (product_id, datetime.now(timezone.utc).isoformat(), status, amount, currency),
    )
    db.commit()


def last_valid(product_id: str):
    return db.execute(
        "SELECT amount, currency FROM observations "
        "WHERE product_id = ? AND status = 'ok' "
        "ORDER BY observed_at DESC LIMIT 1",
        (product_id,),
    ).fetchone()


def check(client: httpx.Client, product_id: str, url: str, threshold: Decimal = Decimal("5")) -> None:
    try:
        response = client.get(url)
    except httpx.HTTPError:
        record(product_id, "network_error")
        return
    if response.status_code != 200:
        record(product_id, f"http_{response.status_code}")
        return
    offer = find_offer(response.text)
    if offer is None:
        record(product_id, "no_offer")
        return
    try:
        amount = Decimal(str(offer["price"]))
    except InvalidOperation:
        record(product_id, "bad_price")
        return
    if amount <= 0:
        record(product_id, "bad_price")
        return
    currency = offer.get("priceCurrency")
    previous = last_valid(product_id)
    record(product_id, "ok", str(amount), currency)
    if previous and previous[1] == currency:
        old = Decimal(previous[0])
        change = (amount - old) / old * 100
        if abs(change) >= threshold:
            print(f"{product_id}: {old} -> {amount} {currency} ({change:.1f}%)")


products = {"catalog-1842": "https://shop.example/products/SKU-492"}

with httpx.Client(timeout=20, headers={"User-Agent": "price-monitor/1.0"}) as client:
    for product_id, url in products.items():
        check(client, product_id, url)
        time.sleep(random.uniform(2, 5))

The script stores a classified status instead of a guessed price, so a consent page, a redirect, or a missing offer never enters the price series. It does not follow redirects, because httpx leaves follow_redirects off by default and a 301 to another host should be reviewed. Pages that expose an AggregateOffer with lowPrice instead of price land in no_offer, which is the right outcome until you decide how to handle ranges.

To run it every six hours, add a cron entry such as 0 */6 * * * python3 /opt/monitor/check.py. For routing through a proxy, pass it when you create the client, as the httpx proxy docs show: httpx.Client(proxy="http://username:password@host:port"). Persistent job queues, retries, and a real alert channel come later. Get the observation history right first.

Validate observations before detecting changes

Validation prevents parser failures from becoming business alerts. Check that:

Add range checks, but do not use them to erase legitimate changes. A 90% drop may be a flash sale, a unit mismatch, a monthly payment, or a parser error. Store it as quarantined, collect supporting evidence allowed by your retention policy, and require confirmation before alerting.

Measure data quality independently from transport success. Track valid observation rate, missing-price rate, unexpected-page rate, product-mismatch rate, parser error rate, and alert confirmation rate. A 99% HTTP success rate is meaningless if half the accepted prices refer to the wrong offer.

Detect meaningful price changes

Compare the new result with the last valid observation for the same product, market, variant, seller, offer type, and currency. Comparing unlike offers is a common source of false alerts.

Useful rules include:

Calculate percentage change against the previous valid price:

percentage change = ((new price - old price) / old price) × 100

Use confirmation rules for noisy sources. Two matching observations several minutes apart can prevent a transient partial render from triggering an alert. The tradeoff is slower detection, so reserve confirmation for changes where false positives are costly.

Price validation gates where confirmed observations trigger alerts and invalid or unclear observations are quarantined

Design alerts for action, not volume

An alert should explain what changed and why it matters. Include:

Deduplicate on product, offer, rule, and change event. Do not send the same alert on every unchanged run after a threshold is crossed. Close or update an event when the price returns above the target, the item becomes unavailable, or a newer valid change replaces it.

Keep quarantined observations out of customer-facing channels. Route them to an operations queue with the parser version and failure reason so the team can distinguish a real price event from a source change.

Handle failures and backoff

Classify responses before retrying:

Result Likely meaning Action
200 with wrong content Consent, challenge, redirect, or parser mismatch Quarantine; inspect the page class
403 Access, authorization, or policy denial Stop automatic retries and review access
429 Request rate is too high Honor Retry-After, reduce concurrency, and cool down
5xx Temporary source or upstream failure Retry a limited number of times with backoff and jitter
Timeout Network, proxy, source, or rendering delay Identify the slow layer before retrying

The HTTP definition of 429 Too Many Requests explains that a server may include Retry-After. Treat it as a minimum wait, not a suggestion to resume every worker simultaneously.

Put retries back into the scheduler with an attempt count and next-run time. Sleeping inside a worker wastes capacity and makes shutdowns harder. Cap retries, add jitter, and use a circuit breaker when one source begins failing broadly.

Which proxies fit price monitoring

Proxies can support legitimate price monitoring when observations vary by region, cloud workers need controlled outbound routing, or independent jobs need isolated network identities. They do not grant permission, repair a broken parser, or justify ignoring access controls.

The rule for retail monitoring is short. Use ISP proxies when you check a fixed set of retailers from one market on a steady schedule, because a static, dedicated IP gives you consistent latency and one route per job. Use residential proxies when the price depends on the shopper's location, because you can check the same product from several countries or cities.

Match routing to the job:

The best proxy for web scraping guide compares these options by target strictness, session needs, speed, and cost. For a fixed set of retailers in one market, compare ISP proxy plans. If broad location coverage is central, residential proxies offer country, state, and city targeting, and the data usage calculator estimates the bandwidth a schedule needs.

Restock monitoring on large retailers such as Target or Walmart is a related but harder job. Monitors often check every few seconds during a product release, and the retailer's bot protection is stricter than on most catalog pages. The sneaker proxies guide covers ISP vs residential setups for release-day traffic. Use the same delay calculator to match monitor delays to task and proxy counts. For a Target-specific bot setup, the Refract Bot guide covers monitor proxies and delays.

Before increasing proxy pool size, reduce duplicate work, slow the scheduler, cache stable pages, and fix synchronized retries. A larger pool can hide poor request discipline without improving data accuracy.

Test the complete monitoring system

Use saved, redacted fixtures when your policy permits them. Your test set should cover:

Run parser tests on every adapter change. Then replay observations through normalization, comparison, deduplication, and alert rules. A selector unit test alone cannot catch an alert that compares a standard offer with a subscription offer.

Deploy source adapters independently. Canary a new parser on a small product subset, compare valid observation rates with the previous version, and keep a rollback path. Alert on sudden shifts in missing fields or page classifications before users report bad data.

FAQ

How often should prices be monitored?

Use the slowest interval that still supports the business decision. Fast-changing, high-value products may need frequent permitted checks, while stable items can be checked daily or less often. Adjust the interval from observed change frequency and the limits the source sets.

Should a price monitor use a browser?

Only when a permitted field is missing from the initial HTML, the embedded JSON-LD, and any approved API. HTTP clients are simpler, faster, and cheaper. Prove that rendering is necessary before adding a browser.

Should price monitoring include shipping?

Include shipping when the metric is total customer cost, and keep it separate when the metric is shelf price. Whichever rule you choose, apply it to every comparable observation.

Do I need proxies for price monitoring?

Not for a small catalog checked slowly from one market. Add proxies when you need to measure prices from other regions or isolate workloads. A proxy does not give you permission to collect a page and does not fix a parser that reads the wrong price.

It depends on the source, the site's terms, the data collected, and your jurisdiction, so this is not legal advice. Prefer official APIs and licensed feeds, and read the competitor price scraping FAQ and the data scraping legality guide before you collect from public pages.

How do I avoid false price alerts?

Compare only like-for-like offers, quarantine observations that fail validation, and require a second matching observation before alerting on large moves. Most false alerts come from a consent page, a variant mismatch, or a unit change, not a real price change.

Final thoughts

Reliable price monitoring depends on identity and validation, not scraping speed. Define the exact offer, store every paced observation, compare only like-for-like records, and quarantine suspicious changes before they become alerts.

Start with a small catalog and a structured source where one exists. Once the alerts are trustworthy, scale the schedule gradually and add proxy routing only where regional measurement or workload isolation is a clear need.

About the Author

Unknown Proxies

Proxy Infrastructure Team

Stay Unknown

High-performance dedicated proxies optimized for speed and reliability. Get uncompromising quality, 99.9% uptime, and unmatched support. Stay Unknown.

Explore Plans