ScreenshotNeo

BlogHow-to

Schedule Screenshots for Website Monitoring

Build a reliable scheduled screenshot monitor with Playwright, cron, visual diffs, alerts, and a hosted ScreenshotNeo option.

By the ScreenshotNeo team30 September 202610 min read

Schedule Screenshots for Website Monitoring

To schedule screenshots for website monitoring, run a repeatable capture job at a fixed cadence, save each image with its timestamp and capture settings, and compare new images with a controlled baseline when you need change detection. A screenshot archive records what a page looked like. A visual monitor adds comparison rules and notifications when the rendered result changes.

You can build this yourself with Playwright plus an external scheduler such as cron, GitHub Actions, or a cloud job runner. You can also use a hosted capture API when you want browser rendering and capture infrastructure without maintaining browsers. The right choice depends on whether you need custom browser interactions, built-in history and alerts, or a simple API response that your own system stores and analyzes.

1. Decide what “monitoring” means

Start by defining the outcome before choosing a schedule.

Goal Workflow What you must retain
Visual archive Capture each URL on a schedule and store the images Image, timestamp, URL, viewport, and job result
Regression detection Capture, compare against a baseline, and review differences Baseline, candidate image, diff result, and comparison settings
Operational alerting Capture, compare, classify the result, and notify an owner All of the above plus alert status and failure details

A changed image is evidence of a visual difference, not an explanation of its cause. A new promotion, rotating article, personalized content, ad, font-loading change, browser update, or real defect can all produce a diff. Keep a human review step for alerts that can trigger work or affect customers.

2. Choose pages, scope, and cadence

Make a list of exact URLs and decide whether each capture is a viewport, a full page, or a specific element. Capture the same scope on every run. A full-page image is useful for long documentation or marketing pages; an element capture is often better for a checkout panel, pricing table, or dashboard card.

A scheduled capture becomes useful monitoring when each image is stored with its conditions and compared with a controlled baseline.
A scheduled capture becomes useful monitoring when each image is stored with its conditions and compared with a controlled baseline.

Choose a cadence that matches the change you are trying to see. An hourly job may be appropriate for a frequently updated status page; a daily or selected-days schedule may be enough for a brochure site. The examples described by monitoring products range from several-hour intervals to daily and monthly schedules, but there is no universal best interval. A shorter interval creates more artifacts and more reviews.

Record the timezone used by the scheduler. A “daily at 09:00” job is ambiguous unless you specify UTC or a named local timezone. Also record the viewport width and height, device scale factor, browser version, locale, timezone, and any interaction steps. Playwright’s visual comparison documentation warns that output can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Keep the baseline and recurring run environment consistent. See the Playwright screenshot documentation and Playwright visual comparison documentation.

3. Build a scheduled Playwright capture

The following Node.js script captures a URL, writes a timestamped image, and stores a small JSON record beside it. Install Playwright with npm install playwright, then install a browser with npx playwright install chromium.

const { chromium } = require('playwright');
const fs = require('fs/promises');
const path = require('path');

const url = process.env.MONITOR_URL || 'https://example.com';
const outputDir = process.env.OUTPUT_DIR || './captures';
const width = Number(process.env.VIEWPORT_WIDTH || 1440);
const height = Number(process.env.VIEWPORT_HEIGHT || 900);

function stamp(date) {
  return date.toISOString().replace(/[:.]/g, '-');
}

(async () => {
  await fs.mkdir(outputDir, { recursive: true });
  const started = new Date();
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width, height },
    deviceScaleFactor: 1,
    locale: 'en-US',
    timezoneId: 'UTC'
  });
  const page = await context.newPage();

  try {
    await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 });
    await page.screenshot({
      path: path.join(outputDir, `${stamp(started)}.png`),
      fullPage: true,
      animations: 'disabled'
    });
    await fs.writeFile(
      path.join(outputDir, `${stamp(started)}.json`),
      JSON.stringify({ url, capturedAt: started.toISOString(), viewport: { width, height }, browser: 'chromium' }, null, 2)
    );
  } finally {
    await browser.close();
  }
})();

networkidle is useful for pages that load data after the initial HTML, but it can hang on applications that maintain long-lived connections. In that case, wait for a stable selector or use a bounded delay instead:

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('[data-monitor-ready]', { timeout: 30000 });
await page.waitForTimeout(1000);

For a single element, use locator.screenshot():

await page.locator('#pricing-table').screenshot({ path: 'pricing-table.png' });

For responsive monitoring, run the same job once per viewport and put the viewport in the filename and metadata. Do not compare a 375-pixel mobile capture with a 1440-pixel desktop baseline.

4. Add a scheduler

On a Linux host, a cron entry can run the script every day at 09:00 UTC:

0 9 * * * cd /srv/site-monitor && /usr/bin/node capture.js >> monitor.log 2>&1

Use an absolute Node path, a working directory, and redirected logs. The scheduler should report a nonzero exit code when navigation, capture, or storage fails. A job that silently produces no image is not a successful monitor.

In CI, schedule a workflow and persist the capture directory as an artifact. A minimal GitHub Actions shape is:

name: website-monitor
on:
  schedule:
    - cron: '0 9 * * *'
  workflow_dispatch:
jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: node capture.js
        env:
          MONITOR_URL: https://example.com
      - uses: actions/upload-artifact@v4
        with:
          name: website-captures
          path: captures/

For production monitoring, add a notification path for both capture failures and detected changes. A missing image, timeout, blocked page, or authentication failure must be distinguishable from a clean capture with no visual change.

5. Compare images with a controlled baseline

Playwright Test can create a reference image on the first run and compare later runs with toHaveScreenshot(). Keep the browser image, operating system, viewport, and fonts stable. Disable animations and mask regions that intentionally change.

import { test, expect } from '@playwright/test';

test('homepage remains visually stable', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    animations: 'disabled',
    mask: [page.locator('[data-dynamic]')],
    maxDiffPixels: 100
  });
});

A pixel threshold can reduce alerts from antialiasing or small rendering variation, but a high threshold can hide meaningful changes. Tune it against real page behavior and document the reason for each mask or threshold. Never mask the very region you are trying to monitor.

For a custom archive, store at least:

  • Capture timestamp and scheduler timezone.
  • URL, viewport, device scale factor, locale, and timezone.
  • Browser and automation version.
  • Full-page, viewport, or selector scope.
  • Wait conditions and interaction steps.
  • Image checksum, storage path, and job outcome.
  • Baseline identifier and diff result, if comparison is enabled.

6. Handle dynamic pages and authenticated content

Dynamic content is the most common reason a monitor produces noisy results. Decide whether rotating content is part of the signal. If not, block or mask it, freeze a test account’s data, or capture a stable component instead of the whole page. Wait for a page-specific readiness selector rather than relying on an arbitrary delay when possible.

For authenticated pages, use a dedicated monitoring account with the minimum access required. Load cookies or storage state from a protected secret store, never commit them to the repository, and avoid saving screenshots that expose personal or financial data. A monitor should also record an authentication failure separately from a genuine visual change.

Cookie banners, chat widgets, newsletter popups, bot checks, and consent overlays can obscure the page. Handle them explicitly in your browser script or choose a capture service that can remove known overlays before capture. A bot-check page should be classified as a failed capture, not accepted as the new baseline.

7. Reliability checklist

  • Set a navigation timeout and a separate selector or delay timeout.
  • Retry transient network failures with a bounded retry count.
  • Save a failure record containing the URL, error, and timestamp.
  • Keep the browser and OS image pinned so upgrades are deliberate.
  • Use stable fonts and disable animations where appropriate.
  • Alert when the scheduler itself stops running, not only when a diff appears.
  • Retain enough history to identify whether a change was intentional.
  • Test inaccessible URLs, slow pages, redirects, empty responses, and expired credentials.

Do not describe a monitoring setup as reliable until its scheduled job, storage, comparison, and notification paths have all been observed in the environment where it will run.

8. Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF, so your scheduler can call it and store the response. Read the ScreenshotNeo API documentation for the complete parameter list.

Overlays and consent elements can hide the page you intend to monitor, so remove or handle them before capture.
Overlays and consent elements can hide the page you intend to monitor, so remove or handle them before capture.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Put one of these commands in cron, a CI schedule, or your job runner. ScreenshotNeo supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, 12 device presets or any viewport, retina scale, PDF paper size and margins, custom CSS and JavaScript, clicks, selector or network-idle waits, ad and tracker blocking, custom headers and cookies, user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching TTLs, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration.

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and whether the request was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Free includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing provides two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to schedule your first captures.

9. Performance, cost, and retention

Browser automation consumes CPU, memory, and download bandwidth. Reusing a browser process for a batch can reduce startup overhead, while isolating contexts keeps cookies and settings separate. Full-page captures and pages with many lazy-loaded images take longer and produce larger artifacts than a fixed viewport or element capture. Store compressed images and define a retention period before the archive grows without bound.

Hosted pricing should be evaluated using your actual URL count, cadence, viewport count, full-page usage, PDFs, retries, and retention needs. Confirm current allowances, privacy terms, and plan limits directly because vendor plans change. With ScreenshotNeo, only clean shots are billed, while bot checks, blank pages, timeouts, failed loads, and cache hits are free; the X-Page-Verdict and X-Billed headers let your accounting job distinguish outcomes.

Caching can reduce repeated work when you intentionally want the same result within a chosen TTL. Disable or shorten the TTL when the purpose is to observe rapid changes. For high volume, use bulk capture, asynchronous jobs, and signed webhooks so your scheduler does not wait on every render.

10. Troubleshooting common failures

Symptom Likely cause Fix
Blank or partial image Capture ran before client rendering completed Wait for a readiness selector, network idle, or a bounded delay; verify lazy content.
Every run creates a diff Rotating ads, timestamps, animations, or personalized data Mask or block changing regions, freeze test data, disable animations, or capture a stable element.
Timeout at network idle Long-lived analytics or websocket connections Use domcontentloaded plus a specific selector and timeout.
Bot-check screenshot Target blocks automation or rate limits the job Respect the site’s access rules, reduce frequency, classify it as a failed capture, and investigate an allowed access method.
Unauthorized page Expired cookies, token, or custom header Refresh protected credentials, use a dedicated account, and keep secrets outside source control.
Missing scheduled runs Cron environment, timezone, path, or CI schedule issue Use absolute paths, log stdout and stderr, verify timezone, and alert on scheduler heartbeats.
ScreenshotNeo response is not billed Page verdict is a bot check, blank page, timeout, failed load, or cache hit Inspect X-Page-Verdict and X-Billed; fix the target or accept that no clean shot was produced.
Large storage bill Too many full-page images or unlimited retention Set retention, compress artifacts, capture only required elements, and use a suitable cadence.

11. Short FAQ

Should I monitor a full page or one element?

Use full page when layout and content flow matter. Use an element when the signal is a specific component and the rest of the page changes frequently.

How often should screenshots run?

Match the schedule to meaningful change. Start with a cadence that produces a reviewable number of images, then adjust after observing real changes and false alerts.

A timestamped image may help retain a record, but legal or regulatory sufficiency depends on jurisdiction and context. The workflow described here does not establish evidentiary status.

Can visual diffs prove a bug?

No. They show that rendered pixels changed. Review the page and deployment context before classifying the change as a defect.

Can I schedule PDFs as well as images?

Yes. Playwright can capture screenshots, while ScreenshotNeo’s capture_pdf MCP tool and PDF options support scheduled PDF output.