<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: WeBot</title>
    <description>The latest articles on DEV Community by WeBot (webot).</description>
    <link>https://dev.to/webot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15230%2F8970ac14-beac-493f-bc0a-eec43d7a34bb.webp</url>
      <title>DEV Community: WeBot</title>
      <link>https://dev.to/webot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/webot"/>
    <language>en</language>
    <item>
      <title>How to Build a Polite Restock Checker: Polling, Caching and Backoff</title>
      <dc:creator>WeBot</dc:creator>
      <pubDate>Sun, 11 Oct 2026 00:54:19 +0000</pubDate>
      <link>https://dev.to/webot/how-to-build-a-polite-restock-checker-polling-caching-and-backoff-ik9</link>
      <guid>https://dev.to/webot/how-to-build-a-polite-restock-checker-polling-caching-and-backoff-ik9</guid>
      <description>&lt;p&gt;Restock alerts are useful. Flooding a retailer's origin with aggressive scrapers is not. If you are watching Pokémon TCG product pages—or any collectible SKU that vanishes in minutes—you can still get timely signals while staying a good citizen of the web.&lt;/p&gt;

&lt;p&gt;This article walks through the engineering patterns that keep a desktop restock checker polite: conditional HTTP, sensible intervals with jitter, backoff on rate limits, structured stock detection, and de-duplicated notifications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with permission, not code
&lt;/h2&gt;

&lt;p&gt;Before you write a loop, read the retailer's &lt;code&gt;robots.txt&lt;/code&gt; and terms of use. If crawling product pages is disallowed, or the site offers an official stock API or feed, prefer that path. A checker that ignores published rules will eventually get blocked—and it may also be a terms violation.&lt;/p&gt;

&lt;p&gt;Treat every poll as a cost the retailer pays. Design so that cost stays low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer conditional requests over full downloads
&lt;/h2&gt;

&lt;p&gt;Most product pages support caching headers. Send &lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;If-None-Match&lt;/code&gt; and &lt;code&gt;Last-Modified&lt;/code&gt; / &lt;code&gt;If-Modified-Since&lt;/code&gt; so unchanged pages return &lt;strong&gt;304 Not Modified&lt;/strong&gt; with an empty body. You learn "nothing changed" without transferring HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If-None-Match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;If-Modified-Since&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;304&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# unchanged
# Persist r.headers.get("ETag") and r.headers.get("Last-Modified") for next poll
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cache those validators per URL. On a quiet night, most of your traffic should be 304s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Poll on a human cadence—with jitter
&lt;/h2&gt;

&lt;p&gt;Fixed 500 ms loops look like bots because they &lt;em&gt;are&lt;/em&gt; bots. For restock monitoring of a handful of SKUs, intervals measured in &lt;strong&gt;tens of seconds to a few minutes&lt;/strong&gt; are usually enough. Spread load across time with jitter so many clients do not align on the same second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nextDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jitterRatio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;jitterRatio&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseMs&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// e.g. base 45s → roughly 31.5s–58.5s between polls&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;nextDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tune &lt;code&gt;baseMs&lt;/code&gt; to how fast stock actually turns over for that retailer—and to what their infrastructure can reasonably absorb. Faster is not always better if it gets you throttled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back off hard on 429 and 503
&lt;/h2&gt;

&lt;p&gt;When a server says &lt;strong&gt;429 Too Many Requests&lt;/strong&gt; or &lt;strong&gt;503 Service Unavailable&lt;/strong&gt;, stop. Honour &lt;code&gt;Retry-After&lt;/code&gt; if present; otherwise use exponential backoff with a ceiling.&lt;/p&gt;

&lt;p&gt;A simple policy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On 2xx/304: reset the failure streak; schedule the next poll with normal jitter.&lt;/li&gt;
&lt;li&gt;On 429/503: multiply the delay (e.g. 2× each time), cap at something like 15–30 minutes, and never spin.&lt;/li&gt;
&lt;li&gt;On repeated failures: pause that URL and surface a clear status in the UI instead of hammering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This protects both you and the store during sale events when everyone is watching the same page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detect stock from structured data, not layout
&lt;/h2&gt;

&lt;p&gt;Scraping CSS class names breaks whenever marketing redesigns a button. Prefer machine-readable product data—especially &lt;strong&gt;JSON-LD&lt;/strong&gt; &lt;code&gt;Product&lt;/code&gt; / &lt;code&gt;Offer&lt;/code&gt; markup with &lt;code&gt;availability&lt;/code&gt; values such as &lt;code&gt;InStock&lt;/code&gt; or &lt;code&gt;OutOfStock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Parse the JSON-LD blocks, normalise the offer fields you care about (SKU, price, availability, URL), and &lt;strong&gt;diff against the previous snapshot&lt;/strong&gt;. An alert fires only when availability flips in a way you care about (for example, &lt;code&gt;OutOfStock&lt;/code&gt; → &lt;code&gt;InStock&lt;/code&gt;), not when a banner image changes.&lt;/p&gt;

&lt;p&gt;If JSON-LD is missing, fall back to other stable signals the site already exposes for SEO or accessibility—still prefer structured fields over brittle DOM paths. Persist the last known offer state so restarts do not re-alert for the same stock level.&lt;/p&gt;

&lt;h2&gt;
  
  
  De-duplicate alerts and notify once
&lt;/h2&gt;

&lt;p&gt;Desktop notifications are easy to spam. Keep a small alert ledger keyed by something like &lt;code&gt;(retailer, sku, availability_state)&lt;/code&gt; with a short TTL or a "already notified for this in-stock window" flag. When stock goes out again and later returns, you can allow a new alert; while it stays in stock, stay quiet.&lt;/p&gt;

&lt;p&gt;For the notification itself, use the platform APIs (Windows toast / macOS User Notifications) with a concise title, product name, and a deep link the user can open. One clear ping beats five redundant ones during a restock scramble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it together
&lt;/h2&gt;

&lt;p&gt;A respectful checker looks roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the URL is allowed under robots/terms.&lt;/li&gt;
&lt;li&gt;Conditional GET with stored validators.&lt;/li&gt;
&lt;li&gt;On 304: sleep with jitter; on 200: parse JSON-LD offers and diff.&lt;/li&gt;
&lt;li&gt;On stock transition of interest: de-dupe, then desktop notify.&lt;/li&gt;
&lt;li&gt;On 429/503: exponential backoff; never tight-loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this requires defeating bot protection or jumping queues. The goal is a lightweight monitor that behaves like a careful browser refresh—just automated, cached, and rate-aware.&lt;/p&gt;

&lt;p&gt;We use these patterns in &lt;a href="https://webot.site/" rel="noopener noreferrer"&gt;WeBot&lt;/a&gt; for AU/NZ collectibles restock alerts. If you build your own checker, keep the retailers' infrastructure in mind: polite defaults scale better than aggressive ones—for you and for everyone else watching the same aisle.&lt;/p&gt;

&lt;p&gt;— The WeBot team&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
