Tools & Reviews 21 min read

Website Change Detection: Track Web Page Changes

Website change detection rechecked in August 2026: verified prices for changedetection.io, Visualping, Distill.io and Fluxguard, the Google Sheets notification rule that no longer fires, and a Python watcher built on conditional requests.

ST
Scraping.Pro Team
Data collection for business needs
Published: 6 July 2025

Point a monitor at a product page, set it to fifteen-minute checks, and go to bed. By morning you have ninety-six alerts and the price never moved. What moved was a CSRF token in a hidden input, a "viewed 3 minutes ago" string, the order of four recommended products, and an ad slot that rotates on every request.

The failure mode of change detection is not silence. It is noise, and noise is why most people who set up a watcher stop reading its email inside a week. Everything below aims at the part nobody writes about: deciding what counts as a change, and keeping that decision alive after a redesign.

Tools, prices, versions and release dates here were read from each vendor's own site, repository or documentation on 13 August 2026.

What actually changes on a page

The best public measurement of this is old and still the right shape. Adar, Teevan, Dumais and Elsas crawled roughly 55,000 URLs hourly for five weeks and published the results at WSDM 2009 as The Web Changes Everything. Three numbers from it decide how you build a monitor.

A third of the web is inert. In their collection, "18971 pages (34%), displayed no change during the studied interval." Not a small change. None.

The rest move slowly on average and violently in patches. "On average, documents that displayed some change (35997 pages) did so every 123 hours and the average Dice coefficient for the change was 0.794." A handful of news and forum pages churned hourly and dragged the mean around.

The skeleton survives even when the text does not. The paper reports a median element survival rate of 99.8% across the full five weeks. The DOM you write a selector against is the durable part of a page. The bytes are not.

That last number is the design brief. A monitor that hashes the response body measures the least stable thing on offer. One that pins an element and compares its normalized text measures something that outlives a year of content edits.

Cloudflare's engineers made the same point from the other direction on 6 August 2026, describing "billions of requests" from well-behaved bots "re-fetching pages that have not changed" and calling it "an enormous amount of machine effort, attached to no outcome at all."

The four steps, and where each one leaks

A change detector fetches, extracts, compares and notifies, on a schedule. Written that way it sounds solved. Each step has a specific way of failing that costs money.

  1. Fetch. A plain HTTP request costs a fraction of a headless browser render in time and memory alike. Open view-source and search for your number before you reach for Chrome.
  2. Extract. The step that rots. Selectors break on redesign, and the default behaviour of nearly every hand-rolled watcher is to treat "selector matched nothing" as "the value is empty string" and then, on the next run, as "no change." A broken watch looks exactly like a quiet one.
  3. Compare. Where the noise lives. Raw markup carries nonces, timestamps, A/B buckets and randomized carousels, so comparison has to run on normalized text, a parsed number, or a filtered subtree.
  4. Notify. Easy to get wrong at 3 a.m.

If you harden one of the four, harden extraction, and make a missing selector page you louder than a real change. A change you miss for six hours is an inconvenience. A watch that has been silently dead for six weeks is a decision made on stale data.

The cheapest check is the one you never parse

Before writing any comparison logic, ask the server whether anything happened. RFC 9110 defines two validators a server can attach to a response, ETag and Last-Modified, and two request preconditions that use them, If-None-Match and If-Modified-Since. When the precondition shows nothing changed, the server answers 304 Not Modified with no body.

The arithmetic is worth doing once. Ten thousand pages checked every fifteen minutes is 960,000 requests a day. At an unremarkable 2 MB per page that is roughly 1.9 TB of transfer daily, most of it re-downloading identical bytes. A 304 is a few hundred bytes of headers.

Three honest caveats. Plenty of application-rendered pages emit no usable validator, and CDNs in front of personalized pages vary the ETag by cookie or region, producing a fresh one on every request that tells you nothing. A changed ETag means the response changed, not your field, so extraction still has to run. And we could not measure validator support across a page sample for this article, so treat this as a large win where it works rather than a guaranteed one.

Where the pattern pays off unambiguously is APIs. The GitHub REST API allows "60 requests per hour" unauthenticated and 5,000 with a personal access token, which is the difference between polling forty repositories every ten minutes and getting throttled by lunchtime.

Monitor the feed, not the page

Scraping rendered HTML should be the last option, not the first. Four cheaper sources exist, and most monitoring guides skip all of them.

Machine-readable endpoints the vendor already publishes. phpMyAdmin is the example the earlier version of this article reached for, and it is a good one for the opposite reason: its download page notes that "We also publish a variety of formats intended for parsing by scripts to download the latest version." One of them is /home_page/version.json, which returned 5.2.3 dated 2025-10-08 when we fetched it on 13 August 2026. Watching that JSON key is immune to every redesign the marketing site will ever have. Scraping .list-downloads a is not.

Sitemaps. A <lastmod> value is a free change signal across a whole catalogue. Read the sitemaps.org protocol before trusting it: the spec says of changefreq that "the value of this tag is considered a hint and not a command," and plenty of CMS plugins stamp lastmod with the build time of the entire site.

RSS and Atom. Still alive on release pages, blogs, forums and job boards. GitHub exposes releases.atom for every repository with no authentication at all.

Archives, for the history you did not collect. RFC 7089 defines Memento, the HTTP framework for time-based access to prior resource states, with Accept-Datetime on the request and Memento-Datetime on the response. It reconstructs what a page said last March without your having monitored it, which is the difference between "we think the terms changed in the spring" and a dated snapshot.

The tools, checked in August 2026

The old guard is gone: Page2RSS, InfoMinder, Femtoo. What follows still runs, with the evidence for saying so.

changedetection.io

Open source, self-hostable, and the default recommendation for anyone comfortable with Docker. The repository sits at about 32,000 stars, and the current tag is 0.55.7, released 25 May 2026. Filtering covers XPath 1 and 2, CSS selectors, and JSONPath or jq for API responses, with a visual element picker for people who would rather not write any of that. Browser Steps let you log in, accept a cookie banner or add an item to a cart before the comparison runs, which is what separates it from the CLI tools. It also does "Re-stock & Price detection for single product pages" with upper and lower bounds, and tracks text, file size and checksums inside PDFs.

Notifications go through Apprise. The project's own site advertises "over 85 notification types"; Apprise itself shipped 1.12.0 on 4 July 2026 announcing "more than 150 services." The hosted plan is $8.99/month for up to 5,000 URLs, with recheck from "5~ minutes (per runner)" and a real Chrome browser included. AI change summaries and plain-English rules landed in the hosted service in June 2026.

One fact outranks every feature above, and no comparison of this tool mentions it. Do not expose a self-hosted instance to the open internet. CVE-2024-32651 was a Jinja2 server-side template injection giving remote command execution, scored CVSS 10.0, affecting 0.45.20 and earlier and fixed in 0.45.21. The pattern did not stop there: the project published six advisories in 2026 alone, three Critical, including a Zip Slip and an XPath arbitrary file read on 4 March, an authentication bypass via decorator ordering on 4 April, and an XXE plus a backup-restore file read on 27 April. The software is fine. Its threat model assumes you are the only one who can reach it.

Visualping

The best-known hosted service, run by WebMonitoring Technologies Inc. of Vancouver, which claims to be "Trusted by 2 million users and teams at 85% of the Fortune 500." Point-and-click selection, visual and text comparison, AI change summaries on every tier.

The free plan deserves a correction, because "generous" is the word every roundup uses, including the earlier version of this article. Read from the pricing page: 150 checks per month across 5 pages at a 60-minute interval. Five pages, thirty days: that is one check per page per day and not one more. Paid tiers start at $14/month, or $10 billed annually, for 1,000 checks across 10 pages at 15-minute intervals; the largest published plan is Business 50K at $350/month ($250 annually) for 50,000 checks, 500 pages and 2-minute intervals.

Note the billing unit. Visualping charges for checks, not pages, so a 2-minute interval burns 21,600 checks per page per month. Every plan is a page count multiplied by an interval, dressed as a quota.

Distill.io

Operated by Neemb LLC, and the one browser-based product with genuine signs of life: the Chrome extension is at version 4.1.0, updated 4 May 2026, with roughly 400,000 users and a 4.5 score across about 1,500 ratings. Local monitors run in your browser, cloud monitors on Distill's servers, and conditional alerts fire only when a parsed value crosses a bound.

From the pricing page: free gives 25 monitors of which 5 may be in the cloud, at a 6-hour cloud interval with 1,000 cloud checks, 30 email alerts and 100 push notifications per month. Starter is $15/month or $180/year for 50 monitors at a 10-minute cloud interval, Professional $35/month or $420/year for 150 monitors at 5 minutes, and Flexi starts at $80 for 500-plus monitors at 2 minutes.

Fluxguard, now part of LegitScript

The compliance option, and it changed hands. Fluxguard now carries a "© 2026 LegitScript LLC" footer, and LegitScript's own page runs a section headed "LegitScript Acquires Fluxguard." No acquisition date is published.

Its pricing unit is sites per month rather than checks. From app.fluxguard.com/pricing: free covers 3 sites, Standard is $110/month for 25 sites, Plus $220/month for 50 sites with AI summarization and a proxy network, and Premium $550/month for 100 sites with 5-minute crawling. That is $5.50 per monitored site at the top tier, priced for defacement monitoring and regulatory evidence rather than volume.

Hexowatch, Wachete, Versionista

Three that are alive but publish less than they should.

Hexowatch, from Hexact, Inc., advertises twelve monitor types, several of which nobody else offers: WHOIS, technology stack, sitemap, backlink and RSS feed monitoring alongside visual and HTML checks. Its pricing page renders entirely client-side and returned no figures to a plain fetch on 13 August 2026, so we have no verified prices to quote.

Wachete is live and its FAQ is direct about the billing model: "Wachete does not impose any monthly check cap" and "you pay only for the number of pages you monitor and the interval at which we check them." Its pricing page did not resolve for us on the same date.

Versionista carries a "© 2007–2026 Versionista · Made in Oregon" footer and headlines its homepage "Monitor Website Changes for Free," while publishing no plan prices at all. People reach for it when the requirement is a defensible archive rather than an alert.

urlwatch, and why you should install its fork instead

This is the recommendation that has aged worst. urlwatch is still the elegant answer for cron-based monitoring: a YAML list of jobs, filters, and a diff in your inbox. It is not archived, and its master branch carries an UNRELEASED changelog section with contributed features waiting on a tag. Its own CHANGELOG dates the last shipped version, 2.29, to 28 October 2024, and the repository has published no GitHub releases at all.

The maintained line is webchanges, a fork of urlwatch 2.21 dated 30 July 2020, relicensed MIT, at 3.37.0 released 2 August 2026. It reads the same job and configuration files and documents the upgrade path. Start a new command-line watcher there.

Browser extensions: a closing chapter

Chrome finished off most of this category on schedule. Google's own Manifest V2 timeline records that with Chrome 138 on 24 July 2025 "all users on all channels of Chrome have now Manifest V2 extensions disabled" and "can no longer turn them back on," and that on 31 August 2026 all remaining Manifest V2 extensions are removed from the Chrome Web Store. That is eighteen days from this article's check date.

The casualty is the extension every roundup still lists. Page Monitor sits at version 4.0.2, last updated 22 April 2024, with about 40,000 users, and its own developer notice reads: "Google is discontinuing SQL and, in spite of our best efforts, we are unable to deploy a version that is compatible with the new versions of Chrome." The listing directs users to Visualping's extension instead. Calling it "still fine for casual use," as this article previously did, was wrong by roughly two years.

On Firefox, Update Scanner is a gentler case: version 4.4.0 dated 24 April 2019, about 3,800 users, rated 4 out of 5 by 195 reviewers. Nothing is broken and nothing is moving.

Huginn, the self-hosted Zapier with about 50,000 stars, also still turns up in these lists. Its most recent release is tagged v2022.08.18. Its website agent works if you already run it; it is not where you would start in 2026.

What the hosted options actually cost

Read the units before the numbers. No two vendors here sell the same thing, and the gap between "checks" and "pages" and "sites" is where the surprise on your invoice comes from. All figures read from the vendors' own pricing pages on 13 August 2026, and rate cards move without announcement.

Service Cheapest paid tier What it buys Free tier
changedetection.io hosted $8.99/mo up to 5,000 URLs, recheck from ~5 min self-host instead
Visualping $14/mo ($10 annual) 1,000 checks/mo, 10 pages, 15 min 150 checks/mo, 5 pages, 60 min
Distill.io $15/mo ($180/yr) 50 monitors, 10-min cloud interval 25 monitors, 5 in cloud, 6 hours
Fluxguard $110/mo 25 sites/mo, 25 versions per page 3 sites/mo
Hexowatch not readable without JavaScript 12 monitor types not readable
Wachete not published on a page we could reach pages multiplied by interval, no check cap exists, limits unpublished
Versionista not published compliance archiving advertised, terms not published

Self-hosting changes the shape of the bill rather than removing it: a small VPS plus your time, and a Chrome fetcher container hungry for memory on every concurrent render.

DIY option 1: Google Sheets, and the part that quietly stopped working

For a single value on a static page, a spreadsheet still beats installing anything. IMPORTXML pulls a value out of a page with an XPath expression:

code
=IMPORTXML("https://example.com/product/101", "//span[@class='price']")

Then the classic recipe says: turn on Tools → Notification settings → Edit notifications, choose "Any changes are made," and Google emails you when the value moves.

It does not, reliably, and this is where the earlier version of this article and most of the recipes still circulating skate past a documented limitation.

Refresh is tied to the document being open. Google's own help page on import functions states that IMPORTDATA, IMPORTHTML and IMPORTXML "automatically check for updates every hour while the document is open, even if the formula and sheet don't change." A tab you closed on Friday is not a monitor.

Externally sourced values are excluded from the newer notification feature by design. Google has since added conditional notifications under Tools → Conditional notifications, which fire when a cell changes and can test the value first. Its documentation lists when they will not fire, and the relevant line is unambiguous: "If your data comes from outside sources, like Connected Sheets or other documents, changes there won't trigger notifications." It adds that volatile functions "recalculate with any worksheet change, potentially causing notifications to miss updates that happen while the document is closed." The feature is also limited to certain work and school accounts.

The honest version of the Sheets recipe is narrower than the one you have read elsewhere. It works if you keep the sheet open in a pinned tab, accept hourly granularity, and verify with a deliberate test change that alerts actually arrive.

For the spreadsheet without the caveats, drive it from Apps Script: a time-driven trigger, UrlFetchApp for the fetch, a cell for the last known value, and MailApp.sendEmail when they differ. That runs on Google's schedule whether or not anyone has the file open.

DIY option 2: a Python watcher built for production

The pattern is unchanged: fetch, extract, compare, alert. What follows adds the three things a toy version omits: conditional requests, a loud failure when the selector stops matching, and state that outlives the process.

Code below was written against Python 3.13, httpx 0.28.1 and selectolax 0.4.11. Two version notes matter. httpx has shipped no stable release since 0.28.1 on 6 December 2024, is still classified Beta on PyPI, and has left 1.0 at development previews since September 2025, so pin it. And selectolax now prefers its Lexbor backend over the legacy Modest engine that HTMLParser wraps, so import LexborHTMLParser.

python
import json, os, sys
import httpx
from selectolax.lexbor import LexborHTMLParser

WATCHES = [
    {"name": "phpMyAdmin latest release",
     "url": "https://www.phpmyadmin.net/home_page/version.json",
     "json_key": "version"},
    {"name": "Competitor pricing",
     "url": "https://example.com/pricing",
     "selector": "#plan-pro .price"},
]
STATE_FILE = "snapshots.json"
UA = "change-monitor/1.0 (+https://example.com/about-our-bot)"

def fetch(url, prev):
    headers = {"User-Agent": UA}
    if prev.get("etag"):
        headers["If-None-Match"] = prev["etag"]
    if prev.get("last_modified"):
        headers["If-Modified-Since"] = prev["last_modified"]
    r = httpx.get(url, headers=headers, follow_redirects=True, timeout=30)
    r.raise_for_status()
    return r

def extract(resp, watch):
    if "json_key" in watch:
        return str(resp.json()[watch["json_key"]])
    node = LexborHTMLParser(resp.text).css_first(watch["selector"])
    if node is None:
        raise LookupError(f"selector {watch['selector']} matched nothing")
    return " ".join(node.text().split())

def alert(kind, name, detail):
    print(f"{kind}: {name}\n  {detail}", file=sys.stderr)
    # plug Apprise in here for email, Slack, Discord, ntfy or a webhook

def run():
    state = json.load(open(STATE_FILE)) if os.path.exists(STATE_FILE) else {}
    failures = 0
    for w in WATCHES:
        prev = state.get(w["name"], {})
        try:
            resp = fetch(w["url"], prev)
        except httpx.HTTPError as exc:
            failures += 1
            alert("FETCH FAILED", w["name"], str(exc))
            continue
        if resp.status_code == 304:
            continue
        try:
            value = extract(resp, w)
        except (LookupError, ValueError) as exc:
            failures += 1
            alert("SELECTOR BROKEN", w["name"], str(exc))
            continue
        if prev.get("value") is not None and value != prev["value"]:
            alert("CHANGED", w["name"], f"{prev['value']} -> {value}")
        state[w["name"]] = {"value": value,
                            "etag": resp.headers.get("etag"),
                            "last_modified": resp.headers.get("last-modified")}
    json.dump(state, open(STATE_FILE, "w"), indent=2, sort_keys=True)
    return 1 if failures else 0

if __name__ == "__main__":
    sys.exit(run())

Three deliberate choices. A 304 short-circuits the comparison. A missing selector raises rather than returning an empty string, so a redesign pages you instead of muting you. The process exits non-zero on failure, so your scheduler surfaces it.

Scheduling it, and the bug in the obvious workflow

Cron or a systemd timer is the simple answer. GitHub Actions is the popular one, and the popular version of that workflow is broken in a way that is easy to miss: the runner's filesystem is discarded when the job ends, so snapshots.json never survives to the next run. Every execution reads empty state, decides there is no previous value, and alerts on nothing. Forever.

The fix is to commit the state file back to the repository.

yaml
# .github/workflows/monitor.yml
name: monitor
on:
  schedule:
    - cron: "7,37 * * * *"     # offset from :00 to dodge the rush
  workflow_dispatch:
permissions:
  contents: write
jobs:
  watch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: pip install "httpx==0.28.1" "selectolax==0.4.11"
      - run: python monitor.py
      - name: Persist snapshots
        run: |
          git config user.name  "monitor"
          git config user.email "monitor@users.noreply.github.com"
          git add snapshots.json
          git diff --cached --quiet || git commit -m "state $(date -u +%FT%TZ)"
          git push

Four details from GitHub's own documentation decide whether this keeps running.

  • The shortest interval is five minutes, and scheduled runs "may experience delays during periods of high load." Treat the cron as a floor, and never schedule on the hour with everybody else.
  • "Scheduled workflows are automatically disabled when no repository activity has occurred for more than 60 days" in public repositories. A monitor that commits its own state resets that clock every run. One that does not dies silently on day 61, which is the most on-brand failure in this entire article.
  • actions/checkout is at v7.0.0; the @v4 in older tutorials still runs, three majors behind.
  • actions/cache, at v5.0.5 from 13 April 2026, is the wrong tool for this state. Entries are immutable per key, capped at 10 GB per repository, and GitHub "will remove any cache entries that have not been accessed in over 7 days."

Two upgrades take this further. For pages that build their values in JavaScript, swap the httpx call for Playwright, at 1.62.0 released 31 July 2026, bundling Chromium 151, Firefox 153 and WebKit 26.5. For sites that push back against frequent automated hits, route through rotating proxies and, where a challenge appears, a captcha-solving service. The anti-blocking playbook is the one any web scraping project uses, with a twist specific to monitoring: an IP that changes between checks can change the price you see, manufacturing changes that never happened.

What to compare: text, pixels, or a number

  • Normalized text. Strip tags, collapse whitespace, drop the elements you have blacklisted, compare. The right default for nearly everything, and the only one that survives a CSS refactor.
  • A parsed number. Convert to a decimal and alert on a threshold or a percentage move. Ideal for price and stock, and the option with the most edge cases: currency symbols, thousands separators that swap meaning between locales, "from $19", and VAT that appears or vanishes depending on the visitor's country.
  • Pixels. Screenshot and diff the images. Catches layout regressions, replaced images and defacement, and it is the noisiest option by a wide margin, because a font swap or a lazy-loaded hero rerenders half the frame. Pair it with a tolerance percentage.

Whatever you compare, add a minimum change threshold, and put the previous value next to the new one in the alert. An alert that says "the page changed" is a request to go and look. An alert that says $149.00 -> $129.00 is a decision.

What breaks between ten pages and ten thousand

Selector rot is the main operating cost, not bandwidth. A redesign takes out every watch on that site at once. Track a "last successful extraction" timestamp per watch and alarm when it goes stale, independently of whether the value changed. This one measure catches more real breakage than everything else combined.

Personalization manufactures phantom changes. Geo-priced pages, A/B buckets held in a cookie, logged-out recommendations and warehouse-specific inventory all produce diffs that are artefacts of your request rather than facts about the site. Pin locale, currency and exit region, and hold cookies stable across checks.

Flapping needs debouncing. Require a change to persist across two consecutive checks before notifying, or batch into a digest.

Storage grows faster than you think. Full snapshots of ten thousand pages at 2 MB each are 20 GB per generation. Store the extracted value always, the normalized text usually, and full HTML only where you will need to prove what the page said.

The unit economics flip. Ten thousand pages checked hourly is 7.2 million checks a month, 144 times Visualping's largest published tier. Past a few hundred URLs the question stops being which plan and becomes whether to run the infrastructure or buy the output. A data-as-a-service feed delivers the fields instead of the plumbing, most often as competitor price monitoring or review monitoring. Targets that actively resist collection are where a managed extraction setup stops being a convenience and becomes the only version that stays green.

Where change detection stops working

Anything behind a login you are not entitled to use. The line is legal, not technical. hiQ Labs versus LinkedIn is the case everyone cites for "public data is fair game," and it ended in December 2022 with a $500,000 judgment against hiQ and a permanent injunction, after LinkedIn established potential liability partly on fake-account access to password-protected pages. Meta versus Bright Data went the other way in January 2024, with the Northern District of California rejecting Meta's contract claim over logged-out public data. The pattern is consistent: the logged-out surface is defensible ground, and credentials change the analysis completely.

Sites whose robots.txt says no. RFC 9309, published September 2022, standardized the Robots Exclusion Protocol. Two clauses monitors get wrong: crawlers "SHOULD NOT use the cached version for more than 24 hours," so a long-running watcher has to re-read the file, and when robots.txt is unreachable through server or network errors "the crawler MUST assume complete disallow." Treating a 500 as permission misreads the standard.

Pages that are not the source of truth. If a regulator publishes through an API and a portal, monitor the API. If a company announces on a blog and in an 8-K, the filing is authoritative and the blog is marketing.

Personal data. Watching a page is data collection. If the field identifies a person, GDPR applies to your snapshot store whether or not the page was public.

Anything you cannot test. If you cannot deliberately cause a change and watch the alert arrive, you do not have a monitor. You have a cron job.

How to choose

Non-technical, a handful of pages, no appetite for running anything: Visualping if you like point-and-click and can live with the check quota, Distill.io if you want conditional alerts and an extension that is still being updated.

Technical, wanting control and no per-page fees: self-host changedetection.io, behind authentication, patched. It is the only tool here combining a visual picker, JSON and XPath filtering, browser steps and price detection, and $8.99 a month for the hosted version undercuts everything else if you would rather not run it.

Command line, cron or CI: webchanges. Not urlwatch, however elegant, until it tags a release.

Compliance and evidence: Fluxguard or Versionista, priced accordingly.

Everything else: twenty lines of Python against a JSON endpoint, on a scheduler that commits its own state.

The bottom line

Change detection is not hard to build. It is hard to keep honest. The tooling has consolidated around one strong open-source project, two competent hosted services, a compliance tier and a command-line tool whose torch has passed to its fork, while the browser-extension era ends in the Chrome Web Store on 31 August 2026.

Pick the narrowest signal that answers your question: a JSON field over an element, an element over a page, a page over a screenshot. Make the monitor shout when it can no longer find what it was watching. Then change something on purpose and confirm the alert lands.