DEV Community

Greta
Greta

Posted on

Cost per Data Point: The Unit Economics of a Price-Monitoring Pipeline

Cost per Data Point: The Unit Economics of a Price-Monitoring Pipeline

A team I worked with ran a price-monitoring pipeline over 10,000 SKUs on a flat schedule: every SKU, every 10 minutes. That sounded reasonable in the planning doc. Then the first full monthly proxy bill arrived.

The arithmetic they had never done: 10,000 SKUs × 4,320 fetches each (a 30-day month at 10-minute intervals) is 43.2 million fetches. A compressed product page runs about 250 KB, so that's roughly 10.3 TB of proxy traffic before retries. Residential proxy bandwidth runs $3–5/GB, so at $4.50/GB the clean fetches alone cost about $46,400. An 8% retry rate adds another ~825 GB and ~$3,700. Add amortized scheduler and storage infra and the bill lands a bit above $50,000 per month. For a dashboard that mostly confirms prices haven't changed.

The problem was not the proxy vendor, and it was not the rate card. The problem was that nobody on the team could answer one question: what does a single trustworthy price observation for a single SKU cost us? Until you can state that number — mine came out to about $0.0012 — every decision about polling frequency, retries, and proxy tiers is a guess. This article builds that cost model explicitly, then shows how it drives architecture, because the architecture decisions are where almost all the savings live.

The only number that matters: cost per observation

Strip away the dashboards and a price monitor does one thing repeatedly: acquire one page, extract one price. Price that unit. The formula:

cost_per_observation = page_gb × attempts × price_per_gb + per_session_cost
attempts             = 1 + retry_rate × retry_bw_fraction
Enter fullscreen mode Exit fullscreen mode

Walk it with real numbers:

  • Bandwidth: 250 KB is 0.000244 GB. At $4.50/GB residential, a clean fetch costs $0.0011.
  • Retries: an 8% failure rate where each retry burns a full page multiplies bandwidth by 1.08, so $0.00119 per delivered observation.
  • Sessions: if your provider charges per-session or per-request gateway fees, add that here; traffic-billed residential usually makes it zero.
  • Infra: scheduler, queue, storage, alerting — say $150/month. Amortized over millions of fetches it's rounding error per observation, but it belongs in the monthly projection.

So: one trustworthy observation ≈ $0.0012 on residential. Sounds microscopic. Multiply by 43.2 million and it's a car payment — every month. The value of the unit number is that every engineering argument becomes arithmetic. Faster polling on a tier? Multiply. Cheaper proxy tier? Substitute the rate and the retry rate. It ends the "feels about right" school of scheduling.

The model, as code

Below is the cost model we actually use, in stdlib Python. It projects monthly cost for any set of polling tiers, and includes a tiering optimizer that assigns polling frequency by observed volatility: SKUs whose price moved in the last 7 days go to a fast tier, SKUs that moved within 30 days to a medium tier, stable SKUs to a slow tier.

# cost_model.py -- unit economics of a price-monitoring pipeline.
# Stdlib only, Python 3.8+.

from dataclasses import dataclass

MINUTES_PER_MONTH = 30 * 24 * 60  # 43,200


@dataclass(frozen=True)
class ProxyTier:
    name: str
    price_per_gb: float  # USD per GB of proxy traffic
    retry_rate: float    # fraction of fetches that fail once and get retried


@dataclass(frozen=True)
class PollingTier:
    name: str
    sku_count: int
    interval_min: int    # minutes between fetches of the same SKU
    page_kb: float       # compressed size of one product page


class MonitoringCostModel:
    """cost_per_observation =
         page_gb * attempts * price_per_gb + per_session_usd
       attempts = 1 + retry_rate * retry_bw_fraction
    """

    def __init__(self, proxy, infra_monthly=150.0,
                 retry_bw_fraction=1.0, per_session_usd=0.0):
        self.proxy = proxy
        self.infra_monthly = infra_monthly          # scheduler, storage, alerting
        self.retry_bw_fraction = retry_bw_fraction  # a retry burns a full page
        self.per_session_usd = per_session_usd      # session/gateway fees, if any

    def attempts_per_observation(self):
        return 1.0 + self.proxy.retry_rate * self.retry_bw_fraction

    def cost_per_observation(self, page_kb):
        page_gb = page_kb / (1024 * 1024)
        return (page_gb * self.attempts_per_observation()
                * self.proxy.price_per_gb) + self.per_session_usd

    def project(self, label, tiers):
        fetches = gb = bandwidth = 0.0
        for t in tiers:
            n = t.sku_count * (MINUTES_PER_MONTH // t.interval_min)
            fetches += n
            gb += n * self.attempts_per_observation() * t.page_kb / (1024 * 1024)
            bandwidth += n * self.cost_per_observation(t.page_kb)
        total = bandwidth + self.infra_monthly
        return {"label": label, "fetches": fetches, "gb": gb,
                "bandwidth": bandwidth, "infra": self.infra_monthly,
                "total": total, "per_obs": total / fetches}


def tier_by_volatility(total_skus, hot_frac=0.08, warm_frac=0.20,
                       hot_min=10, warm_min=60, cold_min=360, page_kb=250.0):
    """Assign polling intervals by price-change recency:
       hot  = price moved in the last 7 days  -> poll fast
       warm = price moved in the last 30 days -> poll hourly
       cold = stable since then               -> poll every 6 hours"""
    hot = round(total_skus * hot_frac)
    warm = round(total_skus * warm_frac)
    cold = total_skus - hot - warm
    return [
        PollingTier("hot (moved <= 7d ago)", hot, hot_min, page_kb),
        PollingTier("warm (moved <= 30d ago)", warm, warm_min, page_kb),
        PollingTier("cold (stable)", cold, cold_min, page_kb),
    ]


def report(proxy, rep):
    print(f"\n{rep['label']}  [{proxy.name}: "
          f"${proxy.price_per_gb:.2f}/GB, {proxy.retry_rate:.0%} retries]")
    print(f"  fetches/month:        {rep['fetches']:>13,.0f}")
    print(f"  proxy traffic:        {rep['gb']:>13,.0f} GB")
    print(f"  bandwidth cost:       ${rep['bandwidth']:>12,.2f}")
    print(f"  infra (amortized):    ${rep['infra']:>12,.2f}")
    print(f"  total:                ${rep['total']:>12,.2f}")
    print(f"  cost per observation: ${rep['per_obs']:>12.6f}")


if __name__ == "__main__":
    SKUS = 10_000
    flat = [PollingTier("FLAT schedule (everyone @ 10 min)", SKUS, 10, 250.0)]
    tiered = tier_by_volatility(SKUS)

    proxies = [
        ProxyTier("datacenter", price_per_gb=0.70, retry_rate=0.35),
        ProxyTier("residential", price_per_gb=4.50, retry_rate=0.08),
    ]
    for proxy in proxies:
        model = MonitoringCostModel(proxy)
        flat_rep = model.project("FLAT", flat)
        tier_rep = model.project("TIERED", tiered)
        report(proxy, flat_rep)
        report(proxy, tier_rep)
        saved = flat_rep["total"] - tier_rep["total"]
        print(f"  tiering saves ${saved:,.0f}/month "
              f"({saved / flat_rep['total']:.1%} of the flat bill)")
Enter fullscreen mode Exit fullscreen mode

Output:

FLAT  [residential: $4.50/GB, 8% retries]
  fetches/month:        43,200,000
  proxy traffic:            11,124 GB
  bandwidth cost:       $50,056.33
  infra (amortized):    $150.00
  total:                $50,206.33
  cost per observation: $0.001162

TIERED  [residential: $4.50/GB, 8% retries]
  fetches/month:         5,760,000
  proxy traffic:             1,483 GB
  bandwidth cost:       $6,674.18
  infra (amortized):    $150.00
  total:                $6,824.18
  cost per observation: $0.001185
  tiering saves $43,382/month (86.4% of the flat bill)

FLAT  [datacenter: $0.70/GB, 35% retries]
  total:                $9,874.57
  cost per observation: $0.000229

TIERED  [datacenter: $0.70/GB, 35% retries]
  total:                $1,446.56
  tiering saves $8,428/month (85.4% of the flat bill)
Enter fullscreen mode Exit fullscreen mode

Two things jump out. First, tiering cuts the residential bill by 86% — from $50,206 to $6,824. Second, and less obvious: a flat schedule on cheap datacenter proxies ($9,875) costs more than a tiered schedule on expensive residential ones ($6,824). A better architecture beats a better rate card. People spend weeks negotiating $/GB and zero hours on their polling distribution.

Why tiering works: prices are boring

Most SKUs do not change price. In typical retail catalogs, roughly 5–10% of SKUs see a price change in any 7-day window and 15–25% in 30 days; the long tail is static for months. A flat 10-minute schedule spends 90%+ of its budget re-confirming facts it already knows.

The tiered example above — 8% hot at 10 minutes, 20% warm at hourly, 72% cold at 6 hours — fetches 5.76 million pages instead of 43.2 million, an 87% volume cut. What do you lose? For a cold SKU, the worst case is learning about a price change 6 hours late. But cold SKUs are cold because they rarely change. If a cold SKU has a 3% monthly probability of any price change, its expected wrong-price exposure is 0.03 × ~3 hours ≈ five minutes per month. You are paying $0.0007 per SKU-month to eliminate five minutes of average staleness on a product nobody was watching anyway.

Two operational notes. Tier assignment must be automatic and based on your own change log, never a static list: a SKU gets promoted the moment its price moves, and demoted only after N consecutive stable days — hysteresis prevents oscillating SKUs from flapping between tiers. And the hot tier is where freshness has actual business value (competitor repricing, MAP violations, flash sales); that is the tier you spend on.

Costs nobody puts in the model

Soft blocks and decoy pages. A blocked fetch costs you the bandwidth and the retry. A soft-blocked fetch is worse: you pay for a page that looks like a success but isn't the real product page — a cached variant, a bot wall that renders, a geo-redirect. That is money spent to corrupt your dataset. Budget validation (price sanity ranges, page-hash change detection) as part of the cost of a trustworthy observation, because an unverified observation isn't one.

Over-provisioned concurrency. Traffic-billed proxies don't charge for concurrent sessions, but your workers, parsers, and CAPTCHA-solver calls scale with capacity you sized for peak. Sizing concurrency to tiered demand instead of flat demand shrinks that fleet too.

Fetching the full page when 8 KB exists. Many retailers expose a lightweight price/availability fragment or JSON endpoint. Going from 250 KB to 8 KB is a 30× bandwidth cut — larger than any proxy discount you will ever negotiate. The model makes this visible: it is the page_kb term, and it is the cheapest term to attack.

Duplicate fetching across teams. The pricing team and the catalog team scraping the same 10,000 SKUs on separate schedules is a 2× multiplier nobody sees because the bills are split. A shared fetch cache keyed on URL is free money.

Decision rules the model hands you

Datacenter vs residential. On pure arithmetic, datacenter wins more often than people expect: at $0.70/GB with a 35% retry rate, an observation costs $0.00023 versus $0.00116 residential — 5× cheaper. Break-even only arrives when datacenter block rates get catastrophic. But the honest reason to pay for residential is not success rate; it is decoy resistance. A datacenter IP that trips a soft block returns plausible garbage you may not detect, and a wrong price that flows into a repricing engine costs more than any bandwidth line item. Practical rule: use datacenter for public price pages on low-defense retailers, residential for hardened targets, and split the routing by domain — never pay residential rates for a site that doesn't need them.

Dead SKUs. No price change and out of stock for 90 days: drop to a daily heartbeat poll or delist entirely. Delisting is just tiering's final tier, and dead inventory is often 10–20% of a catalog.

Break-even for speed. Moving a tier from 6-hour to 10-minute polling multiplies its fetch volume 36×. That is worth it only when catching a change ~5h50m earlier has value exceeding 35 × $0.0012 × sku_count — trivially true for hero products under MAP monitoring, false for long-tail SKUs.

The model will drift

Every input degrades. Product pages get heavier as retailers inject scripts and A/B variants; retry rates creep upward as anti-bot tightens; the hot-tier share grows as your catalog churns. Treat the model as a forecast, not a law. Once a month, pull the actual GB from your proxy provider's dashboard, divide by the count of validated observations your pipeline recorded, and compare that realized cost per observation against the projection. Alert when actual drifts more than 20% from model. Track cost-per-observation as a first-class production metric, right next to latency and uptime — because it is the one metric that quietly decides whether the pipeline survives contact with the invoice.

Build the cost model first. Let it choose your polling distribution, your proxy routing, and your page weight. The five-figure bill in the opening story is not a proxy-pricing problem; it is a scheduling problem wearing a proxy-pricing costume.

Disclosure: I use Thordata's residential proxies for this project. New users get 500MB free — code thor020 (10% off): https://www.thordata.com/?ls=uXcSHJzx&lk=02-tele

Top comments (0)