How to Capture a Website Screenshot at the Same Time Every Month
Schedule a monthly website screenshot with Playwright and GitHub Actions, choose a timezone, and handle missed runs, storage, and hosted capture.
To capture a website screenshot at the same time every month, use a browser automation script such as Playwright to take the image, then run that script with a scheduler. Set the scheduler’s timezone explicitly. For a self-managed setup, this guide uses GitHub Actions and captures the page at 09:17 UTC on the first day of each month. Scheduled runs can be delayed or dropped during high load, so treat this as a recurring target time rather than an exact-minute guarantee.
1. Choose what to capture
Decide what the monthly image should show before writing the job:
- Viewport: the currently visible browser area, useful for a consistent above-the-fold snapshot.
- Full page: the full scrollable document, useful for archiving long pages.
- Element: one selected component, such as a pricing panel or dashboard widget.
Also decide whether the page needs authentication, a click, or a page-specific wait. Those requirements belong in the browser script. Use a stable URL and keep the viewport, browser version, operating system, and capture settings consistent if you plan to compare images. Browser rendering can vary with the host OS, browser version, settings, hardware, power source, and headless mode; see Playwright’s visual comparison guidance.
2. Create a runnable Playwright capture
This example saves a full-page PNG with a date-based filename. It uses Node.js and Playwright. Change the URL and capture mode to fit your page.
npm init -y
npm install playwright
npx playwright install chromium
Create capture.mjs:
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
const url = process.env.TARGET_URL ?? 'https://example.com';
const outputDir = process.env.OUTPUT_DIR ?? 'screenshots';
const date = new Date().toISOString().slice(0, 10);
const outputPath = `${outputDir}/${date}.png`;
await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const response = await page.goto(url, {
waitUntil: 'networkidle',
timeout: 60_000,
});
if (!response || !response.ok()) {
throw new Error(`Page load failed: ${response?.status() ?? 'no response'}`);
}
await page.screenshot({ path: outputPath, fullPage: true });
console.log(`Saved ${outputPath}`);
} finally {
await browser.close();
}
Run it locally:
TARGET_URL=https://example.com node capture.mjs
networkidle can be unsuitable for pages with long polling or persistent network activity. If it times out, wait for a meaningful selector or use a short fixed delay after navigation instead:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('main').waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: outputPath, fullPage: true });
For a viewport screenshot, omit fullPage: true. For a specific element, use await page.locator('.pricing-table').screenshot({ path: outputPath });. Playwright documents viewport, full-page, and element screenshots.
3. Schedule the capture monthly with GitHub Actions
Add .github/workflows/monthly-screenshot.yml to the repository’s default branch. This schedule runs at 09:17 UTC on day one of each month:
name: Monthly website screenshot
on:
schedule:
- cron: '17 9 1 * *'
workflow_dispatch:
permissions:
contents: write
jobs:
capture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: node capture.mjs
env:
TARGET_URL: https://example.com
- name: Commit screenshot archive
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add screenshots/
if ! git diff --cached --quiet; then
git commit -m "Save monthly website screenshot"
git push
fi
Replace https://example.com with the target page. The workflow commits each image to the repository; for a large or long-running archive, use external object storage or another retention system instead. The workflow needs write permission to commit. If you only want to inspect the run artifact, upload the image as a workflow artifact instead of committing it, and configure retention to match your archive needs.
GitHub scheduled workflows use UTC by default and its documentation describes timezone-aware schedules using IANA timezone identifiers. The workflow must be present on the default branch. Check the current GitHub schedule documentation for supported timezone syntax and scheduling behavior; scheduled events may be delayed during high load, and sufficiently high load can cause queued runs to be dropped.
4. Configure timezone and cadence
A five-field cron expression has this order: minute, hour, day of month, month, day of week. For example, 17 9 1 * * means 09:17 on the first calendar day of every month, in the scheduler’s timezone.
| Desired cadence | Cron expression | Meaning |
|---|---|---|
| First day, 09:17 | 17 9 1 * * |
09:17 on day one each month |
| 15th day, 18:00 | 0 18 15 * * |
18:00 on day 15 each month |
| Last day of month | Scheduler-specific | Month lengths vary; do not assume day 31 means the final day |
Use UTC if a stable global time matters. If the capture must follow a local civil time, account for daylight saving changes and set an IANA timezone where the scheduler supports it. Verify that the scheduler accepts the syntax you choose. For a local cron daemon, the schedule is interpreted in the machine’s configured timezone unless otherwise configured; keep that host setting documented.
5. Keep an archive and make the run observable
- Use a date-based filename so each run has a distinct output, such as
2026-10-01.png. - Choose storage deliberately: repository, workflow artifact, or object storage. Define retention and access controls.
- Check scheduler run history after setup and periodically thereafter.
- Configure a failure notification if missing a monthly image matters. The example surfaces failures in the Actions run; notifications depend on your repository or monitoring setup.
- Keep browser and operating system versions stable if images will be compared over time.
Capturing the image only creates an archive. Change monitoring requires a separate baseline and comparison step, such as pixel comparison or a visual review process.
6. Other runnable clients and managed scheduling
Playwright can also be triggered by any scheduler that can run a command: a server cron daemon, a CI workflow, or a managed job runner. Keep the capture script independent from the schedule so you can run it manually to debug and retain the same capture behavior each month.
For a server cron daemon, install the script and browser dependencies on the host, then edit the crontab with crontab -e and add:
17 9 1 * * cd /path/to/project && TARGET_URL=https://example.com /usr/bin/node capture.mjs
This runs in the host’s configured timezone. Use absolute paths, ensure the cron user can write to the output directory, and redirect output to a log or monitoring destination so failures are visible.
A hosted scheduled screenshot service can avoid maintaining the browser runner and scheduler. Allscreenshots documents monthly schedules, timezone selection, full-page and screen-size settings, waiting for an element, hiding dynamic elements, CSS injection, and email or webhook delivery. Its documentation says schedules count against a monthly screenshot quota, webhooks are available on every plan, and email is a paid feature on Starter and above; confirm the provider’s current limits and notification terms before relying on them. See its recurring screenshot guide.
7. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. A single GET request captures a URL. This example uses cURL; the API also accepts the parameter names used by other screenshot APIs, which can make switching easier. See the ScreenshotNeo API documentation for the available options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
This call takes the screenshot when requested; use your scheduler to invoke it at the monthly cadence. ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents, including Claude, Cursor, and other MCP clients, the tools take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 screenshots.
Learn about ScreenshotNeo or sign up free for 1,000 screenshots a month, with no card required.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No scheduled run appears | The workflow is not on the default branch, the cron syntax is invalid, or the schedule has not reached its next occurrence. | Confirm the workflow is committed to the default branch, inspect Actions workflow status, and validate the five cron fields and timezone. |
| The screenshot arrives late or not at all | GitHub schedule events can be delayed or dropped under high load. | Check the Actions run history and retry manually when needed. If exact timing is operationally important, choose a scheduler with timing guarantees that meet the requirement and verify its terms. |
| Navigation times out | The page never reaches network idle, a request hangs, or the site is slow. | Use domcontentloaded and wait for a page-specific selector; set a reasonable timeout and surface failures rather than silently saving a partial image. |
| Screenshot is blank or incomplete | The page needs more render time, lazy content has not loaded, or a selector is wrong. | Wait for the relevant element to become visible, verify the URL and page state, and test the capture locally. For long pages, confirm the intended full-page mode. |
| Monthly images differ unexpectedly | Dynamic content or browser and host rendering differences changed. | Keep runtime settings fixed, use a consistent environment, and hide or stabilize volatile content when appropriate. |
| Commit step fails | The workflow lacks repository write permission, branch protection blocks pushes, or no screenshot was created. | Check workflow permissions and repository rules; confirm the output directory and ensure the script completed successfully. |
| Image is overwritten | The filename is constant or date calculation uses an unexpected timezone. | Use a date-bearing filename and decide whether the date should represent UTC or the schedule’s local timezone. |
9. Performance, reliability, and cost
A monthly run is infrequent, but browser startup and page loading still determine how long the job occupies its runner. Install only the browser you need, set navigation and selector timeouts, and avoid waiting for network idle on pages that never become idle. Full-page captures of very long documents use more memory and produce larger files than viewport or element captures.
Self-managed capture has no screenshot-service fee, but it uses your compute, storage, and maintenance time. Include retention, notification, and rerun needs in the design. GitHub Actions schedule timing is not guaranteed to the minute; review run history and maintain a recovery path if a missed monthly capture matters. A hosted service trades some control for vendor-managed scheduling and browser infrastructure; check current quota, storage, delivery, and timing terms before adopting one.
10. FAQ
Will cron run at the same local time after daylight saving changes?
That depends on the scheduler and its timezone configuration. UTC stays fixed; local civil time can shift relative to UTC. Use an explicitly supported IANA timezone when local time is the requirement.
Does one monthly screenshot show what changed?
No. It records an image. To identify changes, preserve a baseline and run a separate comparison or review process.
Can I capture a page that requires a login?
Yes, if the browser job establishes the required authenticated state securely. Store credentials as scheduler secrets, never in the script or repository, and add the login flow or saved browser state before navigation to the target page.
Should I choose a viewport or full-page image?
Choose the viewport for a compact, consistent first-screen record; choose full page to archive the whole document. Capture an element when only one component matters.


