DEV Community

0xGollum
0xGollum

Posted on

What breaks when you scrape Steam Community Market at scale

CS2 Skin Pulse tracks price and volume signals on CS2 skins from the Steam Community Market. No official API, no key required — but the public JSON endpoints behind it were built for a browser making occasional requests, not a cloud worker hitting them on a schedule. Here's what actually broke, and what fixed it.

1. Datacenter IPs get rate-limited hard

Steam doesn't just slow you down. On Apify's shared IP ranges, priceoverview starts returning 429s on nearly every request, even at low volume — direct requests basically never succeed from there. The fix is routing through Apify's residential proxy, which resolves as normal consumer traffic instead of a flagged datacenter block. On top of that, requests are throttled deliberately (a semaphore capped at 4 concurrent calls, ~350ms sleep between price fetches) instead of relying on the proxy alone to absorb the load.

2. There's no historical price endpoint if you're not logged in

Steam has a pricehistory endpoint, but it requires an authenticated session — not viable for a public actor. So instead of asking "what's the trend," the actor asks "what changed since last time": every run persists each item's price and volume to a named key-value store, then diffs against the previous run to compute price_spike / price_drop / volume_spike signals. Same pattern already used in Bitcoin Pulse for funding-rate history — once you build a "diff against last run" primitive, it works for any source that has no history API at all.

3. Retry logic has to know the difference between "try again" and "give up"

429/500/502/503/504 get retried with backoff. A 403 is a hard block — hammering the same IP again wastes the run's time budget for nothing. And when even priceoverview (rate-limited harder than plain item search) comes back empty after retries, there's a fallback to a lighter search/render call that still returns a current sell price. No median, no volume, but enough to keep the signal alive instead of silently dropping the item for that run.

None of this is specific to CS2 skins — it's the shape of any public JSON endpoint sitting behind a site built for a browser, not a script. Source's public on the Apify Store if you want to see the actual code: https://apify.com/0xgollum/cs2-skin-pulse

Top comments (1)

Collapse
 
roamproxy profile image
RoamProxy

The diff-against-last-run trick is underrated. Half the "there's no history endpoint" problems go away once you just persist each run and compare, no auth needed.

Also +1 on separating retry-worthy from fatal. Treating 429 and 403 the same cost me an afternoon once.