Screenshot Automation: How to Capture Web Pages on a Schedule

javascript dev.to

Screenshot automation for web pages comes down to two pieces: something that fires on a schedule, and something that renders the page. You almost certainly already have the first piece. The second is where people get stuck, because the obvious answer, running headless Chromium yourself, means owning a browser fleet to produce a single image on a timer.

This guide covers the scheduled-capture pattern: how to wire a cron job to a screenshot API, how to pick an interval, how to keep captures consistent enough to compare, and where to store the results.

One clarification first, because search results for this topic mix two unrelated jobs. Capturing your own desktop or phone screen on a timer needs a local screen-capture utility. Capturing a web page on a timer, from a server, with no one at the keyboard, is what this post is about.

The pattern: scheduler plus render

Every scheduled screenshot setup has the same three stages:

  1. Trigger. A cron entry, a GitHub Actions schedule, a Vercel or Cloudflare cron, or a workflow tool fires on an interval.
  2. Render. One HTTP request per URL returns a hosted image.
  3. Store. Write the image URL plus a timestamp somewhere you can query later.

The trigger is a solved problem in whatever stack you already run. The render stage is the only real decision, and it comes down to whether you run the browser or someone else does.

Running the browser yourself

The DIY version is a scheduled script that launches Chromium through Puppeteer or Playwright, navigates to the URL, calls page.screenshot(), and uploads the result. It works, and for a single URL on a laptop it is genuinely fine.

The cost shows up when it has to run unattended. A scheduled job means a long-lived environment, which means keeping the browser alive across runs or paying the launch cost every time. Serverless runtimes do not ship the native libraries Chromium expects, so you end up on @sparticuz/chromium or a custom container. Chromium leaks memory over long runs, so scheduled jobs that never restart eventually get killed. None of these are unsolvable, but they are all maintenance on infrastructure whose entire output is a PNG.

The honest rule: if your scheduled job needs to log in, click through a flow, or fill a form before the capture, you need your own browser automation and this maintenance is the price of the job. If it just needs to render a URL, you do not.

Running it as an API call

If the deliverable is a rendered image of a URL, the render stage collapses into one request. A cron entry, once a day at 6am:

0 6 * * * curl -sS https://api.grabbit.live/v1/grabs \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "width": 1280,
    "full_page": true,
    "delay_ms": 1000,
    "format": "webp"
  }' >> /var/log/pricing-snapshots.log
Enter fullscreen mode Exit fullscreen mode

The response includes an image_url pointing at the stored capture. That URL is the artifact: append it to a log, insert it into a table, post it to Slack. There is no browser on your side, nothing to keep alive between runs, and nothing to patch.

The parameters are the consistency controls, and they matter more in a scheduled pipeline than in a one-off capture:

  • width (320 to 1920) pins the viewport. Fix it and the layout is identical every run, so a diff shows real changes rather than reflow.
  • full_page captures the entire document instead of just the visible viewport, so a change below the fold is not invisible to your pipeline.
  • delay_ms (0 to 10000) waits after load for lazy-loaded images and animations to settle. This is the single most common fix for scheduled captures that look different every run for no reason.
  • format accepts png, jpeg, or webp. For an archive that accumulates one image per run, webp keeps storage down.
  • selector captures a single element instead of the page. Scoping a monitor to #pricing-table removes noise from unrelated sections of the page.

Scheduling more than one URL

Most real automations track a list, not a single page. Loop the list in the scheduled job and make one request per URL. Keep two things in mind:

Grabbit rate-limits to 60 requests per minute per team, so a list longer than about 60 URLs needs to be paced rather than fired in one burst. For a nightly job this is a non-issue; add a short sleep between calls and it stays well under.

Second, be deliberate about the identifier you store. A screenshot is only useful later if you can find it, so record the URL, the capture timestamp, and the returned image_url together. Overwriting a single "latest.png" throws away the history that made the automation worth building.

There is a fuller walkthrough of the batch case in how to screenshot a list of URLs.

Picking an interval

The interval should track how fast the content actually changes, not how often you would like to look at it:

What you are capturing Reasonable interval
Your own app, for visual regressions On every deploy, not a clock
Competitor pricing or marketing pages Daily
News, dashboards, status pages Hourly
Legal or compliance archiving Daily, retained long term
A page that changes weekly at most Weekly

Running more often than the page changes produces a pile of identical images and a bill that scales with nothing. The exception is your own application, where the right trigger is not a clock at all: capture on deploy, because that is the moment the page can change.

From automation to monitoring

A scheduled capture on its own is an archive. Add an image diff between consecutive captures and it becomes a monitor that tells you when something changed, which is usually the actual goal. pixelmatch handles strict pixel comparison and resemblejs does a more forgiving perceptual comparison for pages with anti-aliased text.

The consistency parameters above are what make that diff trustworthy. A pipeline with a floating viewport width or no settle delay will produce diffs on every run and you will stop reading the alerts within a week. Monitoring a website for visual changes covers the diff and alerting side in full.

Cost, honestly

Scheduled automation is the case where per-request pricing compounds, so it is worth doing the arithmetic before you set the interval. Grabbit is $0.002 per live grab. Fifty URLs captured daily is 1,500 grabs a month, or about $3. The same fifty URLs hourly is roughly 36,000 grabs a month, closer to $72. The interval, not the tool, is what drives the number.

Credits are prepaid and do not reset monthly, which suits scheduled work specifically: a job that runs unevenly, or that you pause for a quarter, does not forfeit anything at a billing boundary. Test-environment keys return placeholder images at no cost, so you can wire and debug the full schedule before any real capture runs. Other providers list lower per-grab rates, so if raw unit price is the only variable that matters to you, compare directly.

Where to go next

If you are building scheduled capture into a product, automated screenshots covers the API side in more depth. If you are starting from a single URL and have not automated anything yet, how to screenshot a website from a URL is the shorter starting point.


Originally published on the Grabbit blog.

Source: dev.to

arrow_back Back to Tutorials