Hand a team one product URL and ask for “the price.” You’ll get a number. Ask again from a Canadian residential IP, a UK exit, and a US datacenter — and you may get three numbers, three currencies, and three different “out of stock” stories.
Same path. Same SKU in the URL. Different catalog. That’s geofencing, and it quietly breaks a lot of “global” scrapes.
Why sites do this
Merchants route assortment, tax-inclusive pricing, shipping promises, and even which variants exist by market. Sometimes it’s an explicit country picker cookie. Sometimes it’s CDN geo headers. Sometimes the HTML looks identical and only the JSON price blob flips. From the outside it all feels like one site. Downstream, it’s three datasets wearing one hostname.
A tiny truth table beats a vibes check
Before you scale a crawl, pin a handful of URLs and fetch them under controlled exits. Write down what actually changed — not what the homepage language toggle claims.
| url | exit geo | currency | price | in_stock | variant_count |
|-----|----------|----------|-------|----------|---------------|
| /p/1001 | CA | CAD | 49.00 | true | 4 |
| /p/1001 | GB | GBP | 32.00 | true | 3 |
| /p/1001 | US | USD | 39.00 | false | 4 |
If those rows diverge, your warehouse key cannot be url alone. It has to be something like (url, market) or (sku, market) — and every row needs an explicit scraped_market (and usually currency) column.
Sticky geo is a session rule, not a nice-to-have
Treat market the way you’d treat an authenticated login: one sticky exit (or one consistent geo pool) for the life of the session. Mixing a CA cookie jar with a US IP mid-job is how you get half a Canadian cart and half a US price list in the same extract.
- Warm up on the market homepage or country selector before deep links.
- Persist the locale / country cookies with that exit.
- Rotate between markets across jobs — not inside one parse of a single URL.
- Record the proxy geo you intended and, if you can, the country the site believed you were in (banner text,
hreflang, currency symbol).
async function fetchForMarket(url, market) {
const proxy = await checkoutStickyProxy({ geo: market });
const session = await warmMarket(proxy, market); // homepage / locale cookie
const html = await fetchWithSession(url, session);
return {
url,
market,
currency: readCurrency(html),
...extractProduct(html),
};
}
What “wrong” looks like in the warehouse
- Prices that jump 20% overnight with no merchant promo — you swapped exits.
- Stock that flickers true/false on a schedule — different FCs, not a flaky selector.
- SKUs that exist in US extracts and 404 in CA — assortment is market-scoped.
- Currency symbols stripped so CAD and USD collapse into one numeric column and “look fine” in a chart.
Delivery habits we won’t skip
- Market is a required field — null market is a failed row, not a default.
- One job config per market (or a fan-out that labels each branch), so schedules and budgets stay honest.
- Canary URLs per geo — if the CA canary starts returning USD, stop the CA crawl before you poison a week of history.
- Compare markets on purpose — price gaps are often the product; don’t accidentally average them away.
Geofencing isn’t an edge case for international brands. It’s the default for anyone serious about commerce. Scrape like the catalog is local — because it is — and label every row with where “here” was when you looked.
Need help with a scrape?
Tell us what to extract →