<?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: Maz I</title>
    <description>The latest articles on DEV Community by Maz I (@codewithmaz).</description>
    <link>https://dev.to/codewithmaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4069321%2F1791ed1f-1a10-4123-8273-387a0ea02340.png</url>
      <title>DEV Community: Maz I</title>
      <link>https://dev.to/codewithmaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codewithmaz"/>
    <language>en</language>
    <item>
      <title>Building a Website Change Monitoring System: From One URL to a Distributed Crawling Pipeline</title>
      <dc:creator>Maz I</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:27:40 +0000</pubDate>
      <link>https://dev.to/codewithmaz/building-a-website-change-monitoring-system-from-one-url-to-a-distributed-crawling-pipeline-471k</link>
      <guid>https://dev.to/codewithmaz/building-a-website-change-monitoring-system-from-one-url-to-a-distributed-crawling-pipeline-471k</guid>
      <description>&lt;p&gt;&lt;strong&gt;Building a Website Change Monitoring System: From One URL to a Distributed Crawling Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine someone gives us a URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tell me if this website changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, this sounds almost trivial.&lt;/p&gt;

&lt;p&gt;Fetch the page, save the HTML, come back later, fetch it again, compare the two versions.&lt;/p&gt;

&lt;p&gt;For one page, that can work.&lt;/p&gt;

&lt;p&gt;But almost immediately, questions start appearing.&lt;/p&gt;

&lt;p&gt;What exactly are we monitoring?&lt;/p&gt;

&lt;p&gt;Only the homepage?&lt;/p&gt;

&lt;p&gt;Every page under the website?&lt;/p&gt;

&lt;p&gt;How often should we check it?&lt;/p&gt;

&lt;p&gt;Every two minutes?&lt;/p&gt;

&lt;p&gt;Once per day?&lt;/p&gt;

&lt;p&gt;Only once when an administrator requests it?&lt;/p&gt;

&lt;p&gt;What happens when a harmless calendar or rotating banner changes?&lt;/p&gt;

&lt;p&gt;And if we eventually monitor hundreds or thousands of websites, who decides &lt;strong&gt;what should be crawled&lt;/strong&gt;, and who actually performs all that crawling?&lt;/p&gt;

&lt;p&gt;Those questions are what turn a simple script into a monitoring system.&lt;/p&gt;

&lt;p&gt;This article walks through that evolution one design decision at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  First question: what does “monitor this website” actually mean?
&lt;/h2&gt;

&lt;p&gt;Suppose we start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we fetch only that URL, we are monitoring only the homepage.&lt;/p&gt;

&lt;p&gt;But the important page might actually be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/products/important-product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So monitoring a website introduces our first configuration decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  How deeply should we crawl?
&lt;/h2&gt;

&lt;p&gt;Consider this website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
│
├── About
├── Products
│   ├── Product A
│   └── Product B
│
└── Blog
    ├── Post 1
    └── Post 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we monitor only the homepage, we might completely miss a change on &lt;code&gt;Product A&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So we could tell the crawler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Follow every internal link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But that creates another problem.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Product A&lt;/code&gt; may contain links to documentation.&lt;/p&gt;

&lt;p&gt;Documentation may contain hundreds of pages.&lt;/p&gt;

&lt;p&gt;Those pages may link to thousands more.&lt;/p&gt;

&lt;p&gt;Suddenly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then potentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10,000+ URLs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we need a boundary.&lt;/p&gt;

&lt;p&gt;That is what &lt;strong&gt;crawl depth&lt;/strong&gt; gives us.&lt;/p&gt;

&lt;p&gt;If the administrator chooses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
    ↓
direct internal links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage
    ↓
Level 1 pages
    ↓
links discovered inside Level 1 pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Depth 0

example.com
      |
      v

Depth 1

/about
/products
/blog
      |
      v

Depth 2

/products/a
/products/b
/blog/post-1
/blog/post-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the administrator can choose the monitoring surface instead of letting the crawler wander indefinitely.&lt;/p&gt;

&lt;p&gt;This also tells us something important about our data model.&lt;/p&gt;

&lt;p&gt;A website isn't just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;url
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs configuration.&lt;/p&gt;

&lt;p&gt;Something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website
├── URL
├── Crawl depth
├── Monitoring frequency
├── Ignore rules
├── Notification preferences
└── Monitoring enabled/disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are no longer building a crawler.&lt;/p&gt;

&lt;p&gt;We are beginning to build a &lt;strong&gt;monitoring product&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second question: how often should we check?
&lt;/h2&gt;

&lt;p&gt;Suppose our baseline crawl finishes at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When should we crawl again?&lt;/p&gt;

&lt;p&gt;There is no universally correct answer.&lt;/p&gt;

&lt;p&gt;For a low-risk informational website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;once per day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be enough.&lt;/p&gt;

&lt;p&gt;For something sensitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 5 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be appropriate.&lt;/p&gt;

&lt;p&gt;For another system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;every 2 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might be required.&lt;/p&gt;

&lt;p&gt;And sometimes continuous monitoring isn't even needed.&lt;/p&gt;

&lt;p&gt;An administrator may simply want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl once now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So frequency also belongs to the website's monitoring configuration.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com

crawl depth: 2
frequency: every 5 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;another-site.com

crawl depth: 1
frequency: daily
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates our next architectural requirement.&lt;/p&gt;

&lt;p&gt;Something needs to keep asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which websites are due to be checked now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That responsibility belongs to a &lt;strong&gt;scheduler&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Website configuration
        |
        v
    Scheduler
        |
        v
Is next crawl due?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scheduler doesn't need to know anything about HTML parsing or link discovery.&lt;/p&gt;

&lt;p&gt;Its job is simply to decide &lt;strong&gt;when work should happen&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That separation becomes important later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third question: how do we avoid becoming the attacker?
&lt;/h2&gt;

&lt;p&gt;Now imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl depth = 3
frequency = 2 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the website contains thousands of pages.&lt;/p&gt;

&lt;p&gt;A badly designed crawler could start firing hundreds of requests against the same server every two minutes.&lt;/p&gt;

&lt;p&gt;Our monitoring system could accidentally create the exact availability problem it is supposed to help detect.&lt;/p&gt;

&lt;p&gt;So monitoring frequency cannot mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hit the website as aggressively as possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The crawler needs to behave responsibly.&lt;/p&gt;

&lt;p&gt;A mature design should consider things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;per-domain concurrency limits
request delays
timeouts
maximum pages per crawl
retry backoff
429 handling
5xx handling
overlapping crawl prevention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com

maximum concurrent requests = 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than letting dozens of workers hammer the same domain simultaneously.&lt;/p&gt;

&lt;p&gt;And if the previous crawl is still running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl #101 = running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we probably shouldn't blindly start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl #102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just because the next two-minute interval arrived.&lt;/p&gt;

&lt;p&gt;Already, the “simple crawler” has acquired scheduling and resource-control requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fourth question: what happens when hundreds of websites become due together?
&lt;/h2&gt;

&lt;p&gt;Suppose we have 1,000 monitored websites.&lt;/p&gt;

&lt;p&gt;At 10:00, hundreds become eligible for crawling.&lt;/p&gt;

&lt;p&gt;One naive implementation would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard
   |
   v
Fetch website
   |
   v
Parse HTML
   |
   v
Save results
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now the application serving administrators is also responsible for long-running crawling work.&lt;/p&gt;

&lt;p&gt;That creates several problems.&lt;/p&gt;

&lt;p&gt;A slow website could tie up the main application.&lt;/p&gt;

&lt;p&gt;A timeout could delay unrelated work.&lt;/p&gt;

&lt;p&gt;A burst of scheduled crawls could overload the dashboard server.&lt;/p&gt;

&lt;p&gt;And scaling the crawler would mean scaling the entire application.&lt;/p&gt;

&lt;p&gt;So logically, we separate two responsibilities.&lt;/p&gt;

&lt;p&gt;The main application manages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customers
websites
configuration
monitoring settings
administration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crawler handles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network requests
HTML
link extraction
snapshots
change detection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the system I worked on, the main application kept its structured application data in &lt;strong&gt;MySQL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Crawler-oriented data was handled separately.&lt;/p&gt;

&lt;p&gt;But now these two parts need a way to communicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is where the queue appears
&lt;/h2&gt;

&lt;p&gt;Instead of telling the crawler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Crawl this website right now and make me wait until you're done,&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the application can publish a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"websiteId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;into a message queue.&lt;/p&gt;

&lt;p&gt;Now the architecture evolves naturally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Dashboard
   |
   v
 MySQL
   |
   v
Scheduler
   |
   v
Crawl Queue
   |
   v
Crawler Workers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us a buffer.&lt;/p&gt;

&lt;p&gt;If 500 crawl jobs appear suddenly, the main application doesn't need 500 crawlers immediately.&lt;/p&gt;

&lt;p&gt;Jobs wait in the queue.&lt;/p&gt;

&lt;p&gt;Crawler workers consume them according to available capacity.&lt;/p&gt;

&lt;p&gt;If we later need more crawling throughput, we can add crawler workers without redesigning the dashboard.&lt;/p&gt;

&lt;p&gt;The queue isn't there because queues are fashionable.&lt;/p&gt;

&lt;p&gt;It appears because &lt;strong&gt;scheduled work and execution capacity are different problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now the crawler finally receives a URL
&lt;/h2&gt;

&lt;p&gt;Suppose the worker receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first operation is straightforward.&lt;/p&gt;

&lt;p&gt;Fetch the page.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&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;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&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;But this is the first crawl.&lt;/p&gt;

&lt;p&gt;There is nothing to compare against yet.&lt;/p&gt;

&lt;p&gt;So instead of detecting a change, this crawl establishes a &lt;strong&gt;baseline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We store the page content.&lt;/p&gt;

&lt;p&gt;In our crawler side, page snapshots and crawling data were stored in &lt;strong&gt;MongoDB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now the architecture has two distinct data concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MySQL
↓
website/customer/configuration data

MongoDB
↓
crawler/page/snapshot-oriented data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation wasn't simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;SQL good here, MongoDB good there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two sides represented different workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The homepage is only the beginning
&lt;/h2&gt;

&lt;p&gt;Once the homepage is downloaded, we inspect its internal links.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Products&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/blog"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Blog&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We extract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/about
/products
/blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;convert them to full URLs where necessary, and keep only links belonging to the target website.&lt;/p&gt;

&lt;p&gt;Then we need another important step:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;deduplication&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Imagine the same page appears through several navigation paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Homepage → Products
Blog → Products
About → Products
Footer → Products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We don't want to crawl &lt;code&gt;/products&lt;/code&gt; four times during the same crawl run.&lt;/p&gt;

&lt;p&gt;So logically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract links
      |
      v
Normalize URLs
      |
      v
Remove duplicates
      |
      v
Check crawl depth
      |
      v
Schedule inner pages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner crawler then processes those URLs.&lt;/p&gt;

&lt;p&gt;Each inner page can discover more URLs.&lt;/p&gt;

&lt;p&gt;So a queue item might carry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"depth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;depth &amp;lt; maxDepth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we extract more links.&lt;/p&gt;

&lt;p&gt;When:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;depth == maxDepth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we stop expanding.&lt;/p&gt;

&lt;p&gt;That simple number prevents recursive discovery from turning into uncontrolled crawling.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL deduplication is trickier than it looks
&lt;/h2&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/about
https://example.com/about/
https://example.com/about#team
https://example.com/about?utm_source=email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are those four separate pages?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;But often they represent the same useful monitoring target.&lt;/p&gt;

&lt;p&gt;So before deduplicating, a modern crawler may normalize URLs by handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fragments
tracking parameters
relative paths
trailing slashes
canonical URLs
host casing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, the crawler may spend significant resources repeatedly monitoring effectively identical pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now we finally reach the actual monitoring problem
&lt;/h2&gt;

&lt;p&gt;After the first crawl, suppose we stored this page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com/products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five minutes later, according to its configured frequency, the scheduler queues the website again.&lt;/p&gt;

&lt;p&gt;The crawler downloads the page again.&lt;/p&gt;

&lt;p&gt;Now we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;previous version
current version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to know:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Has anything changed?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In our implementation, one simple mechanism was an &lt;strong&gt;MD5 hash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML
 |
 v
MD5
 |
 v
hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So baseline HTML produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A7F91...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the next crawl produces another hash.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash == new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the inputs are identical.&lt;/p&gt;

&lt;p&gt;Nothing changed.&lt;/p&gt;

&lt;p&gt;If:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash != new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;something changed.&lt;/p&gt;

&lt;p&gt;Very simple.&lt;/p&gt;

&lt;p&gt;Very fast.&lt;/p&gt;

&lt;p&gt;And also incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Because “different” does not mean “important”
&lt;/h2&gt;

&lt;p&gt;Imagine this element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    9 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A day later it becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    10 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTML changed.&lt;/p&gt;

&lt;p&gt;Therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old MD5 != new MD5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system alerts the administrator.&lt;/p&gt;

&lt;p&gt;But there was no attack.&lt;/p&gt;

&lt;p&gt;Nothing important happened.&lt;/p&gt;

&lt;p&gt;Now imagine another page contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:31:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and one minute later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:32:04
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other common examples include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rotating headlines
advertisements
visitor counters
timestamps
calendars
live market values
random identifiers
dynamic widgets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reveals the central weakness of pure hashing.&lt;/p&gt;

&lt;p&gt;A hash can answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Are these two inputs identical?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It cannot answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this difference meaningful?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To MD5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calendar date changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;attacker replaced the homepage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are both simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;different input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that creates &lt;strong&gt;false positives&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why false positives are dangerous
&lt;/h2&gt;

&lt;p&gt;Imagine monitoring a security-sensitive website.&lt;/p&gt;

&lt;p&gt;The administrator receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT
ALERT
ALERT
ALERT
ALERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;throughout the day.&lt;/p&gt;

&lt;p&gt;Most alerts are harmless calendar or headline changes.&lt;/p&gt;

&lt;p&gt;Eventually the administrator begins ignoring them.&lt;/p&gt;

&lt;p&gt;Now when an actual unexpected modification happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ALERT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it looks like everything else.&lt;/p&gt;

&lt;p&gt;So false-positive handling isn't merely a convenience feature.&lt;/p&gt;

&lt;p&gt;It directly affects whether the monitoring system remains useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing the ignore list
&lt;/h2&gt;

&lt;p&gt;Suppose we know this region changes constantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"calendar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    10 August
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The administrator can configure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ignore .calendar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ignore .ticker
ignore #clock
ignore .rotating-banner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now our comparison pipeline becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fetched HTML
     |
     v
Apply ignore rules
     |
     v
Remove known dynamic regions
     |
     v
Generate normalized content
     |
     v
MD5
     |
     v
Compare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of hashing everything blindly, we're hashing the content we actually care about monitoring.&lt;/p&gt;

&lt;p&gt;Conceptually:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_for_monitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignored_selectors&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ignored_selectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then:&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="n"&gt;normalized_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_for_monitoring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.calendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.ticker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;page_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a calendar update doesn't automatically become a security alert.&lt;/p&gt;

&lt;p&gt;This is where the system moves from:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;change detection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;toward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;meaningful change detection&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  But what happens when a meaningful change is detected?
&lt;/h2&gt;

&lt;p&gt;Suppose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;old_hash != new_hash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;after ignore rules have been applied.&lt;/p&gt;

&lt;p&gt;The crawler has discovered something.&lt;/p&gt;

&lt;p&gt;Should it now send an email itself?&lt;/p&gt;

&lt;p&gt;Send an SMS itself?&lt;/p&gt;

&lt;p&gt;Update the dashboard itself?&lt;/p&gt;

&lt;p&gt;It could.&lt;/p&gt;

&lt;p&gt;But then the crawler would be responsible for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP crawling
HTML parsing
comparison
email
SMS
dashboard updates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's too many responsibilities in one component.&lt;/p&gt;

&lt;p&gt;So another boundary naturally appears.&lt;/p&gt;

&lt;p&gt;The crawler emits a &lt;strong&gt;change event&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change detected
      |
      v
Alert Queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then separate notification handlers can deliver it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Alert Queue
                  |
       +----------+----------+
       |          |          |
       v          v          v
   Dashboard    Email       SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an SMS provider outage doesn't stop crawling.&lt;/p&gt;

&lt;p&gt;Email delivery can retry independently.&lt;/p&gt;

&lt;p&gt;And a new notification channel can be added later without rewriting the crawler.&lt;/p&gt;

&lt;p&gt;Again, the queue isn't introduced because “event-driven architecture is cool.”&lt;/p&gt;

&lt;p&gt;It appears because &lt;strong&gt;detecting something and notifying someone are separate reliability problems&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture we ended up with
&lt;/h2&gt;

&lt;p&gt;By following the requirements rather than starting from technologies, our simple URL checker evolved into something closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Administrator
                         |
                         v
                    Dashboard
                         |
                         v
                       MySQL
             website/configuration
                         |
                         v
                     Scheduler
                         |
               Is crawl due?
                         |
                         v
                    Crawl Queue
                         |
                         v
                  Python Crawlers
                         |
              +----------+----------+
              |                     |
              v                     v
        Link Discovery          Page Snapshot
              |                     |
              v                     v
      Normalize/Deduplicate       MongoDB
              |
              v
        Depth-controlled
          inner crawling

                         |
                         v
                  Prepare HTML
                apply ignore list
                         |
                         v
                    Generate MD5
                         |
                         v
               Compare previous hash
                    /        \
                   /          \
              unchanged      changed
                                |
                                v
                           Alert Queue
                                |
                    +-----------+-----------+
                    |           |           |
                    v           v           v
                Dashboard      Email       SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What began as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;download HTML
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;became scheduling, crawling, discovery, state, comparison, noise reduction, queueing, and notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more problem: crawl politely
&lt;/h2&gt;

&lt;p&gt;Let's return to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frequency = 2 minutes
depth = 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose this website has 5,000 discoverable pages.&lt;/p&gt;

&lt;p&gt;We absolutely don't want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5,000 requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fired aggressively against the target.&lt;/p&gt;

&lt;p&gt;A responsible crawler needs controls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-domain concurrency
&lt;/h3&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 workers → example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;we might enforce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;maximum 2 concurrent requests → example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Crawl budgets
&lt;/h3&gt;

&lt;p&gt;A website configuration could include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;max depth: 3
max pages per crawl: 1,000
request timeout: 10 seconds
per-domain concurrency: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depth and page limit solve different problems.&lt;/p&gt;

&lt;p&gt;Depth 1 could still contain 20,000 links.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry with backoff
&lt;/h3&gt;

&lt;p&gt;If the target returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;429 Too Many Requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;retrying immediately is the wrong response.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1s
2s
4s
8s
16s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with jitter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Don't overlap crawls blindly
&lt;/h3&gt;

&lt;p&gt;If monitoring is configured every two minutes but one crawl takes four minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10:00 crawl A starts
10:02 crawl B scheduled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the scheduler should probably detect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;website currently crawling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than blindly launching another run.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change if I designed it today
&lt;/h2&gt;

&lt;p&gt;The underlying problem is still interesting, but I wouldn't rebuild every detail exactly the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hash first, diff second
&lt;/h3&gt;

&lt;p&gt;Hashing is still useful for a very fast first check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash same
    |
    v
stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hash different
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would generate an actual structured diff.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Website changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the administrator might see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- Payment destination: account A
&lt;/span&gt;&lt;span class="gi"&gt;+ Payment destination: account B
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's significantly more useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the DOM, not only serialized HTML
&lt;/h3&gt;

&lt;p&gt;HTML already contains structure.&lt;/p&gt;

&lt;p&gt;So changes could be classified by region:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;title changed
main content changed
form action changed
external script added
navigation changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then different changes could receive different severity levels.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Calendar changed              INFO
Headline changed              LOW
Form action changed           HIGH
New external JavaScript       HIGH
Large DOM replacement         CRITICAL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Better normalization
&lt;/h3&gt;

&lt;p&gt;Before comparison I would also normalize things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whitespace
volatile generated IDs
known tracking parameters
timestamps
irrelevant markup differences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would further reduce noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit crawl-run tracking
&lt;/h3&gt;

&lt;p&gt;Instead of only knowing whether a website is “currently crawling,” I would model crawl runs explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crawl_run_id
website_id
started_at
finished_at
pages_discovered
pages_processed
pages_failed
status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes operational debugging much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dead-letter queues
&lt;/h3&gt;

&lt;p&gt;Some crawl jobs will repeatedly fail.&lt;/p&gt;

&lt;p&gt;After controlled retries, they should move somewhere visible instead of looping forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normal queue
     |
   retries
     |
     v
dead-letter queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;p&gt;For a serious deployment I'd monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queue depth
crawl duration
pages/sec
HTTP error rate
retry count
change rate
false-positive rate
notification failures
per-domain request rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without those metrics, it's difficult to know whether the system is healthy or merely running.&lt;/p&gt;

&lt;h2&gt;
  
  
  And today, AI creates an interesting extra layer
&lt;/h2&gt;

&lt;p&gt;The deterministic monitoring engine should remain deterministic.&lt;/p&gt;

&lt;p&gt;I would &lt;strong&gt;not&lt;/strong&gt; replace HTML comparison with an LLM.&lt;/p&gt;

&lt;p&gt;But after a real change has already been detected, AI could help answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What does this change mean?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;script src="/assets/app.js"&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;script src="https://unknown-example.com/inject.js"&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A semantic analysis stage might classify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"external_script_added"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A previously unseen external JavaScript source was introduced."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deterministic detection
        |
        v
Real change found
        |
        v
Generate structured diff
        |
        v
Rules / classifiers
        |
        v
Optional AI analysis
        |
        v
Severity + explanation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the ordering.&lt;/p&gt;

&lt;p&gt;AI helps interpret the signal.&lt;/p&gt;

&lt;p&gt;It doesn't replace the reliable mechanism that discovers the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting lesson
&lt;/h2&gt;

&lt;p&gt;When we began, the requirement looked like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Monitor a URL for changes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But each real-world question forced another design decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which pages matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ crawl depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How frequently do they matter?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ monitoring schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when hundreds become due?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ queue + workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we revisit inner pages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ link extraction + deduplication + depth tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we know something changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ snapshots + hashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we avoid useless alerts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ ignore rules + normalization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we notify reliably?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ alert events + separate notification workers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we avoid harming the monitored site?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;→ crawl budgets, rate limits, concurrency control, and backoff.&lt;/p&gt;

&lt;p&gt;That's what I find most interesting about systems like this.&lt;/p&gt;

&lt;p&gt;The final architecture doesn't need to be invented on a whiteboard first.&lt;/p&gt;

&lt;p&gt;It can emerge naturally from repeatedly asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What problem does our current simple solution fail to solve next?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>systemdesign</category>
      <category>python</category>
      <category>architecture</category>
      <category>webdelopment</category>
    </item>
    <item>
      <title>Why does PostgreSQL sometimes ignore an index you created?</title>
      <dc:creator>Maz I</dc:creator>
      <pubDate>Sat, 08 Aug 2026 22:55:41 +0000</pubDate>
      <link>https://dev.to/codewithmaz/why-does-postgresql-sometimes-ignore-an-index-you-created-6pa</link>
      <guid>https://dev.to/codewithmaz/why-does-postgresql-sometimes-ignore-an-index-you-created-6pa</guid>
      <description>&lt;p&gt;&lt;strong&gt;Why does PostgreSQL sometimes ignore an index you created?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common assumption is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“If a column has an index, PostgreSQL should use it.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But PostgreSQL does not work that way.&lt;/p&gt;

&lt;p&gt;An index is only one possible access path.&lt;/p&gt;

&lt;p&gt;Before executing a query, PostgreSQL's query planner estimates the cost of different plans and chooses the one it believes will be cheapest.&lt;/p&gt;

&lt;p&gt;That might be:&lt;/p&gt;

&lt;p&gt;• Sequential Scan&lt;br&gt;
• Index Scan&lt;br&gt;
• Index Only Scan&lt;br&gt;
• Bitmap Index Scan&lt;br&gt;
• Parallel Sequential Scan&lt;/p&gt;

&lt;p&gt;So sometimes PostgreSQL sees your perfectly valid index and deliberately decides:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Scanning the table is cheaper.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_users_status&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And imagine the table contains 10 million users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;active   = 7,000,000
inactive = 2,900,000
banned   =   100,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now compare these queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'banned'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both use the indexed &lt;code&gt;status&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;But PostgreSQL may choose very different execution plans.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;banned&lt;/code&gt;, only around 1% of rows match.&lt;/p&gt;

&lt;p&gt;Using the index can make sense because PostgreSQL can locate a relatively small set of rows instead of scanning millions of unrelated rows.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;active&lt;/code&gt;, around 70% of the table matches.&lt;/p&gt;

&lt;p&gt;Now using the index may mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Traverse the index.&lt;/li&gt;
&lt;li&gt;Find millions of matching row locations.&lt;/li&gt;
&lt;li&gt;Visit millions of table pages to retrieve those rows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At that point, reading the table sequentially may simply cost less.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;selectivity&lt;/strong&gt; becomes important.&lt;/p&gt;

&lt;p&gt;A highly selective condition returns a small percentage of the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'user@example.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is usually a great candidate for an index.&lt;/p&gt;

&lt;p&gt;A low-selectivity condition might match most of the table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index may provide little advantage.&lt;/p&gt;

&lt;p&gt;But how does PostgreSQL know how many rows are likely to match?&lt;/p&gt;

&lt;h3&gt;
  
  
  Statistics.
&lt;/h3&gt;

&lt;p&gt;PostgreSQL collects information about the data distribution in a table.&lt;/p&gt;

&lt;p&gt;The planner uses those statistics to estimate things such as:&lt;/p&gt;

&lt;p&gt;• how many rows a condition will match&lt;br&gt;
• common values&lt;br&gt;
• value distribution&lt;br&gt;
• number of distinct values&lt;br&gt;
• relationships that affect selectivity&lt;/p&gt;

&lt;p&gt;That is why stale statistics can result in poor plans.&lt;/p&gt;

&lt;p&gt;You can refresh them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this brings us to one of the most useful tools for PostgreSQL performance work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'banned'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EXPLAIN&lt;/code&gt; shows the plan PostgreSQL intends to use.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; actually executes the query and shows what really happened.&lt;/p&gt;

&lt;p&gt;Two numbers I pay particular attention to are:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated rows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What PostgreSQL thought would happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actual rows&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What really happened.&lt;/p&gt;

&lt;p&gt;If the planner estimates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows = 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but the query actually returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows = 100,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that difference is a clue.&lt;/p&gt;

&lt;p&gt;The planner may be making decisions using an inaccurate picture of the data.&lt;/p&gt;

&lt;p&gt;There is another important misconception:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More indexes do not automatically mean faster databases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every index has a cost.&lt;/p&gt;

&lt;p&gt;When you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL may also need to update the relevant indexes.&lt;/p&gt;

&lt;p&gt;Indexes consume storage, add write overhead, and create additional structures the planner has to consider.&lt;/p&gt;

&lt;p&gt;So the goal is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Index every column used in a WHERE clause.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The better approach is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify a slow query.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Understand the execution plan.&lt;/li&gt;
&lt;li&gt;Check row estimates and actual rows.&lt;/li&gt;
&lt;li&gt;Look at selectivity and data distribution.&lt;/li&gt;
&lt;li&gt;Decide whether the query or index should change.&lt;/li&gt;
&lt;li&gt;Measure again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Composite indexes introduce another layer.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_status&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order of columns matters because the index structure is organized around those columns.&lt;/p&gt;

&lt;p&gt;And sometimes PostgreSQL can combine multiple indexes using bitmap scans rather than using one composite index.&lt;/p&gt;

&lt;p&gt;There are also partial indexes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_pending_orders&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of indexing every order, you can index only the subset important to a particular workload.&lt;/p&gt;

&lt;p&gt;And covering indexes can sometimes allow PostgreSQL to answer a query without visiting the table heap at all.&lt;/p&gt;

&lt;p&gt;The deeper I go into PostgreSQL performance, the more one idea stands out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An index does not tell PostgreSQL what to do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It gives the planner another option.&lt;/p&gt;

&lt;p&gt;The planner still has to decide whether that option is actually cheaper.&lt;/p&gt;

&lt;p&gt;So when PostgreSQL ignores an index, I would not immediately ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Why isn't PostgreSQL using my index?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I would first ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“What does PostgreSQL know about my data that makes another plan look cheaper?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question usually leads to a much more interesting investigation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxglcukfcxhcsb5iz7my.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxglcukfcxhcsb5iz7my.png" alt=" " width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>databaseperformance</category>
      <category>node</category>
    </item>
  </channel>
</rss>
