How to Monitor a Website Screenshot with Playwright and Cron
Capture a website on a schedule, compare screenshots for visual changes, and keep the logs and images needed to investigate failures.
To monitor a website screenshot with Playwright and cron, create a Playwright script that opens the page, waits for a meaningful ready condition, saves a timestamped screenshot, and logs failures. Schedule that script with cron on a machine you maintain, or use a hosted scheduled workflow such as GitHub Actions. If you need to detect visual changes rather than just retain evidence, use Playwright Test’s toHaveScreenshot() assertion and review changed images instead of automatically accepting them as new baselines.
Keep the browser, operating system, viewport, fonts, and page setup consistent between captures. Differences in rendering environments can produce visual noise unrelated to a real site change. Playwright’s visual comparison documentation describes the baseline workflow and these environmental differences.
1. Choose what the monitor should detect
There are two useful monitoring patterns:
- Screenshot archive: save a dated image on each run. This gives you a visual record to inspect, but it does not automatically decide whether the page changed.
- Visual regression check: compare each capture with a retained reference image. A changed image makes the check fail so that someone can review whether the difference is expected.
Decide whether the whole page or just one region matters. A full-page capture is useful for an overall record; an element capture reduces irrelevant changes when you only care about a specific component. Playwright supports page, full-page, element, and buffer screenshots in its screenshots guide.
2. Create a Playwright screenshot monitor
The following Node.js script uses Playwright directly. It accepts the target URL as an environment variable, uses a fixed viewport, waits for a meaningful selector, writes a timestamped full-page PNG, and exits unsuccessfully if navigation or capture fails. It creates the output directory if needed.
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
const targetUrl = process.env.MONITOR_URL;
if (!targetUrl) {
throw new Error('Set MONITOR_URL to the page to monitor.');
}
const readySelector = process.env.READY_SELECTOR;
const outputDir = process.env.SCREENSHOT_DIR ?? 'screenshots';
const viewport = {
width: Number(process.env.VIEWPORT_WIDTH ?? 1440),
height: Number(process.env.VIEWPORT_HEIGHT ?? 1000),
};
if (!Number.isInteger(viewport.width) || !Number.isInteger(viewport.height) ||
viewport.width < 1 || viewport.height < 1) {
throw new Error('Viewport dimensions must be positive integers.');
}
const safeHost = new URL(targetUrl).host.replace(/[^a-zA-Z0-9.-]/g, '_');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const outputPath = path.join(outputDir, `${safeHost}-${timestamp}.png`);
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport });
page.setDefaultNavigationTimeout(45_000);
page.setDefaultTimeout(15_000);
const response = await page.goto(targetUrl, { waitUntil: 'domcontentloaded' });
if (!response) {
throw new Error('Navigation completed without a main-document response.');
}
if (!response.ok()) {
throw new Error(`Main document returned HTTP ${response.status()}`);
}
if (readySelector) {
await page.locator(readySelector).waitFor({ state: 'visible' });
}
await page.screenshot({ path: outputPath, fullPage: true, animations: 'disabled' });
console.log(JSON.stringify({ status: 'captured', url: targetUrl, file: outputPath }));
} catch (error) {
console.error(JSON.stringify({ status: 'failed', url: targetUrl, error: String(error) }));
process.exitCode = 1;
} finally {
await browser.close();
}
Install the dependencies and make the first capture:
mkdir website-monitor
cd website-monitor
npm init -y
npm install playwright
npx playwright install chromium
mkdir -p screenshots
MONITOR_URL='https://example.com' READY_SELECTOR='main' node monitor.mjs
Save the script as monitor.mjs. Replace https://example.com and, if the page has no visible main element, set READY_SELECTOR to a selector that reliably indicates the content you care about. The code checks the main document response, but a successful document response does not guarantee every image or third-party component loaded. Inspect the resulting screenshot and adjust readiness to match the page.
Readiness and screenshot options
waitUntil: 'domcontentloaded'waits for document parsing. Use a visible selector for application content that appears later. An arbitrary fixed delay can be used for a known animation or delayed widget, but it is less reliable than waiting for a meaningful state.- Use
fullPage: trueto capture the full scrollable document. Omit it for the visible viewport only. - To capture one component instead, wait for it and call
page.locator('.price-card').screenshot({ path: outputPath }). - For image processing without first writing a file, call
const image = await page.screenshot(); this returns bytes that can be passed to another step. - To reduce animation-related variation, the example disables animations. You can also choose a fixed device scale factor, color scheme, locale, or timezone when creating the browser context if those settings affect the page.
3. Compare captures with Playwright Test
A timestamped archive helps a person inspect changes. For an automated visual check, use Playwright Test’s screenshot assertion. On its first run, toHaveScreenshot() creates a reference screenshot; subsequent runs compare against it. Retain and review the baseline, and update it only after confirming a change is intentional. See the official visual comparisons guide.
Install the test runner and browser:
npm install --save-dev @playwright/test
npx playwright install chromium
Create playwright.config.ts:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
retries: 0,
use: {
browserName: 'chromium',
headless: true,
viewport: { width: 1440, height: 1000 },
screenshot: 'only-on-failure',
},
});
Create tests/website-visual.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage visual baseline', async ({ page }) => {
const url = process.env.MONITOR_URL ?? 'https://example.com';
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
expect(response, 'main document should respond').not.toBeNull();
expect(response!.ok(), `unexpected HTTP ${response!.status()}`).toBeTruthy();
await page.locator('main').waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled',
});
});
Run it with MONITOR_URL='https://example.com' npx playwright test. The first execution creates the baseline. Inspect and commit the generated snapshot files according to your repository’s review process. Later runs fail when the screenshot differs beyond the assertion’s configured comparison behavior. Avoid an automatic “update snapshots” step in the scheduled job: that would replace the evidence you need to detect a change.
4. Schedule it with local cron
Local cron is a good fit when you control a machine that stays available at the desired run time and want direct control over the runtime and output storage. Cron entries have five time and date fields: minute, hour, day of month, month, and day of week. The crontab manual describes the fields, minute-level checking, and daylight-saving behavior.
- Use absolute paths for the project, Node executable, and log file. Cron does not necessarily start in your interactive shell’s working directory or inherit its environment.
- Confirm the script works from the project directory with the same environment variables the scheduled job will receive.
- Edit your user crontab with
crontab -e. - Add an entry such as the following for a daily run at 03:15 in the machine’s local time zone.
15 3 * * * cd /absolute/path/website-monitor && MONITOR_URL='https://example.com' READY_SELECTOR='main' /usr/bin/node /absolute/path/website-monitor/monitor.mjs >> /absolute/path/website-monitor/monitor.log 2>&1
Find the executable path with command -v node in the environment where Node is installed. If Node is managed by a version manager, cron may not load that manager’s shell initialization; use the actual Node binary path or a wrapper script that initializes the runtime explicitly. Ensure the log directory exists and that the cron user can write to both logs and screenshots.
Cron checks schedules at minute granularity. Local-time jobs can be skipped or repeated around daylight-saving transitions when the scheduled wall-clock time does not exist or occurs twice. For a schedule that must follow a specific time zone consistently, account for the host’s cron implementation and time-zone configuration rather than assuming every local-time transition behaves identically.
5. Schedule it with GitHub Actions
A hosted workflow avoids maintaining a machine that is always available, but runs in the hosted runner environment and follows GitHub’s scheduling behavior. GitHub Actions uses five-field POSIX cron syntax; schedules are UTC by default, can specify an IANA time zone, run against the default branch, and have a documented minimum interval of five minutes. Consult GitHub’s workflow syntax reference for current details.
Commit a workflow file such as .github/workflows/website-monitor.yml to the repository’s default branch. This example runs daily at 03:15 UTC and supports manual dispatch:
name: Website screenshot monitor
on:
schedule:
- cron: '15 3 * * *'
workflow_dispatch:
jobs:
capture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Capture screenshot
env:
MONITOR_URL: https://example.com
READY_SELECTOR: main
run: node monitor.mjs
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: website-screenshots
path: screenshots/
if-no-files-found: ignore
retention-days: 14
This is an illustrative configuration. Pin action versions according to your maintenance policy, choose artifact retention appropriate for your needs, and configure a separate notification path if failures need to page or alert an operator. The artifact upload preserves available images for inspection; it does not itself notify anyone. For a visual assertion job, run npx playwright test and upload the test report and failure attachments as artifacts. Playwright documents its CI setup and notes containers as an option for keeping screenshot environments consistent in its continuous integration guide.
6. Make visual comparisons trustworthy
Screenshot comparison detects pixels, so it can report differences caused by rendering conditions as well as actual content changes. Playwright documents variation from the host OS, browser version, settings, hardware, power source, and headless mode. Use a stable environment for both baseline creation and later runs.
- Fix the browser engine and version, operating system or container image, viewport, device scale factor, locale, color scheme, and timezone where relevant.
- Wait for the same content state each run. Avoid capturing during loading, transitions, or rotating content.
- Use a stable test account and data fixture if the page is personalized or changes based on account state.
- Consider capturing a stable element rather than the full page if timestamps, ads, recommendations, or other dynamic areas create irrelevant noise.
- Review each changed image before updating the baseline. Preserve the old baseline or diff long enough to understand the change.
7. Keep evidence and failures actionable
A screenshot is evidence, not a complete monitoring system. Store the run timestamp, target URL, exit status, logs, and image or diff together. Choose durable storage and retention based on how far back you need to investigate. A local directory alone can fill a disk; a hosted workflow’s artifacts also have a configured retention period.
Make failure visible to the people responsible for the page. Cron can append standard output and errors to a log, and a CI job can retain artifacts and report a failed workflow. Notifications require separate configuration if your monitoring requirement calls for them. Do not discard nonzero exit codes or overwrite the only copy of a changed screenshot.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Works in a terminal but not in cron | Cron has a limited environment, different working directory, or a different Node path. | Use absolute paths, set required environment variables in the crontab or wrapper, and redirect output to a writable log. |
| Browser executable missing | Playwright’s Chromium binary was not installed for the runtime user or environment. | Run npx playwright install chromium locally or npx playwright install --with-deps chromium in the documented CI setup; install for the same user that runs the capture. |
| Navigation timeout | The page is slow, unreachable, or waits on long-lived network activity. | Check connectivity and the main document response. Use a deliberate navigation readiness condition, raise the timeout only when justified, and wait for a specific content selector. |
| Ready selector times out | The selector is absent, hidden, renamed, or appears only after a state the script did not trigger. | Inspect the page and choose a selector that is visible in the intended state; make any required login or interaction explicit. |
| Screenshot is blank or incomplete | The capture ran before app content rendered, a redirect led elsewhere, or the page requires authentication. | Log the final page URL, check response status, wait for the application’s content marker, and supply authorized cookies or credentials through protected environment variables when needed. |
| Visual test fails on every run | Rendering environment or dynamic page content differs from the baseline. | Keep browser and runner conditions consistent, stabilize data and timing, and isolate dynamic regions where appropriate. Review the diff before changing the reference. |
| No screenshot appears in CI artifacts | The script wrote to a different directory, failed before capture, or the artifact path does not match. | Use an explicit output directory, upload that path even on failure, and inspect the job log and exit status. |
| Scheduled workflow does not run | The workflow is not on the default branch, the cron expression is invalid, or schedule timing is misunderstood. | Check the default branch, quote the five-field expression, verify UTC or the configured IANA zone, and account for the documented minimum interval. |
| Disk usage grows over time | Timestamped captures are retained indefinitely. | Set an explicit local cleanup or archival policy, or configure a finite artifact retention period. |
9. Performance, reliability, and cost
Each run starts a browser, loads the target, waits for readiness, and writes an image. The page’s own load time, full-page length, and browser environment determine much of the work; no universal duration applies. Keep the schedule no more frequent than the decision requires, and capture only the page region needed. For multiple targets, consider whether sequential runs fit the available job time before adding concurrency, which can increase resource use and load on the sites being monitored.
Reliability comes from explicit timeouts, nonzero exit codes on errors, durable evidence, a known browser environment, and an operator-visible failure path. A scheduled job can miss a capture if its host or runner is unavailable, so retain run history and decide how to handle gaps. Cost depends on where the browser runs, storage retention, and CI usage under your account; consult the relevant provider’s current terms for your setup rather than assuming a fixed price.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. Make one request to capture a URL; the response can be PNG, JPEG, WebP, or PDF. Its API also supports async jobs with signed webhooks, bulk capture of up to 100 URLs per call, caching with a chosen TTL, and a usage API. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
Cookie banners are accepted like a visitor and 60+ known consent platforms, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with the page verdict and billing status in response headers. An MCP server lets AI agents using Claude, Cursor, or another MCP client take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots, and every feature is on every plan. To run a recurring monitor, schedule the API request with your existing cron or workflow and retain the resulting images and run status.
Create a free account for 1,000 screenshots a month with no card.
FAQ
Does a screenshot archive automatically tell me when a page changed?
No. The direct script saves evidence. Use Playwright Test’s screenshot assertion or another comparison step to detect visual differences.
Should I use cron or GitHub Actions?
Use cron when you control a reliably available host and want local scheduling and storage. Use a hosted workflow when its runner, schedule, and artifact retention fit your needs.
Can I monitor a component instead of the entire page?
Yes. Wait for the component’s locator and call its screenshot method, or structure a visual test around the relevant page area.
Can the monitor capture a page that requires login?
Yes, if you provide the required authenticated browser state securely and handle credentials according to your organization’s secret-management practices.


