How to capture Google SERP screenshots on a rotating schedule with Playwright
Build a scheduled Playwright screenshot workflow for pages you are permitted to capture, with timestamped files, reliable cleanup, and observable runs.
Direct answer: For a target you are expressly permitted to access, use a Playwright script that opens the page, waits for the relevant state, saves a timestamped screenshot, closes the browser in a finally block, and is invoked by a scheduler on your machine or hosted runner. A rotating schedule does not make automated Google Search rank-checking permitted. Google says automated access to Search for rank checking without express permission violates its spam policies and Terms of Service. Use an expressly permitted data source for the intended purpose or obtain permission before automating Search access. See Google’s Search spam policies and Google’s Terms.
1. Check permission before scheduling
This guide shows a general Playwright capture workflow for an allowed target. The code below uses a placeholder URL; it is not an instruction to automate Google Search without permission. Changing the time, frequency, IP address, or browser settings does not establish permission, and this guide does not cover evading bot checks or access controls.
If you are documenting Search with permission, keep the interface realistic and unmodified. Google’s screenshot guidance says not to imply Google endorsement, asks for trademark attribution under Search images, and calls for relevant third-party content approvals where required. It notes that permission is not needed for screenshots in print for educational or instructional purposes, while advertising uses require Google’s approval. Separate third-party rights and other applicable restrictions can still matter. Read Google’s Search screenshot guidelines.
2. Create a repeatable Playwright capture
The script below is a generic example for a permitted page. It uses Node.js and Playwright’s Chromium browser, fixes the viewport, waits for document readiness, writes a timestamped PNG, logs the run result, and always closes the browser. Playwright’s page.screenshot() supports saving a screenshot to a path and offers options for controlling output; see the Page API.
Install
mkdir scheduled-capture
cd scheduled-capture
npm init -y
npm install playwright
npx playwright install chromium
Save this as capture.mjs. Set CAPTURE_URL to a target you are permitted to access. The output directory is configurable with CAPTURE_DIR.
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';
import path from 'node:path';
const url = process.env.CAPTURE_URL;
if (!url) throw new Error('Set CAPTURE_URL to a permitted target URL.');
const outputDir = process.env.CAPTURE_DIR || './captures';
const width = Number(process.env.CAPTURE_WIDTH || 1440);
const height = Number(process.env.CAPTURE_HEIGHT || 1000);
if (!Number.isInteger(width) || width < 1 || !Number.isInteger(height) || height < 1) {
throw new Error('CAPTURE_WIDTH and CAPTURE_HEIGHT must be positive integers.');
}
const safeTimestamp = new Date().toISOString().replaceAll(':', '-');
const outputPath = path.resolve(outputDir, `capture-${safeTimestamp}.png`);
const startedAt = new Date().toISOString();
let browser;
try {
await mkdir(outputDir, { recursive: true });
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width, height },
deviceScaleFactor: 1
});
page.setDefaultNavigationTimeout(45_000);
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.screenshot({ path: outputPath, fullPage: true, type: 'png' });
console.log(JSON.stringify({
status: 'success', scheduledRunAt: process.env.SCHEDULED_RUN_AT || null,
startedAt, finishedAt: new Date().toISOString(), outputPath
}));
} catch (error) {
console.error(JSON.stringify({
status: 'failure', scheduledRunAt: process.env.SCHEDULED_RUN_AT || null,
startedAt, finishedAt: new Date().toISOString(), message: String(error)
}));
process.exitCode = 1;
} finally {
if (browser) await browser.close();
}
Run it manually before connecting a scheduler:
CAPTURE_URL='https://example.com/' node capture.mjs
This verifies that the script can launch in the chosen environment and write a file. It does not verify permission for a different target or the behavior of a particular scheduler.
Wait for the state you need
domcontentloaded is a reasonable starting point for a document screenshot, but it does not guarantee that client-rendered content, fonts, or images have finished. If the permitted page has a specific stable marker, wait for it explicitly after navigation:
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.locator('[data-capture-ready="true"]').waitFor({ state: 'visible', timeout: 15_000 });
await page.screenshot({ path: outputPath, fullPage: true, type: 'png' });
Replace the selector with a marker that the page actually provides. For a permitted page with no marker, a short fixed delay can be used when necessary, but it adds latency and does not prove the page is complete. Avoid relying on a single universal wait condition for every site.
3. Schedule the command
The scheduling system depends on where the script runs. The research for this guide does not establish current syntax for a particular scheduler, so treat the following as the general integration pattern: configure the scheduler to invoke the same command you tested manually, provide the required environment variables, set an explicit working directory, and retain its exit status and logs.
- Choose a machine or hosted runner that is available at the desired times.
- Install a compatible Node.js runtime, the project dependencies, and the Playwright Chromium browser in that environment.
- Configure the scheduler to run
node /absolute/path/to/capture.mjsat the desired cadence. - Set
CAPTURE_URLand any viewport or output-directory variables in the scheduler’s environment. - Record scheduled time, actual start and finish time, status, and output path. Alert on nonzero exit status or missing output.
- Define retention and access controls for captured files, especially if the page can contain private or personal information.
Rotating the execution time can help sample an allowed page at different times, but it should not be presented as a way to avoid enforcement or make prohibited access acceptable. Keep the browser, viewport, locale, and other capture settings consistent when you need comparable images.
4. Screenshot options and consistency
Playwright’s page.screenshot() accepts options such as an output path, image type, fullPage, clipping clip, and transparency through omitBackground for supported formats. Consult the current Page API reference for the complete option set and behavior. These common choices help make recurring captures predictable:
- Viewport: Set width and height explicitly when creating the page. A different viewport can change wrapping and page layout.
- Scale: Set
deviceScaleFactorwhen creating the browser context or page if pixel density matters. Keep it fixed between runs. - Full page:
fullPage: truecaptures beyond the visible viewport. Long pages can produce large images and require more memory. - Format: PNG is lossless and useful for visual comparison. JPEG can reduce size at the cost of artifacts; screenshot options include quality controls for lossy output.
- Clip: Use a defined clip rectangle when only a known region is needed. Ensure the clip coordinates fit the rendered page.
- Animations: Screenshot options can disable finite animations during capture. This can improve repeatability, but does not make changing content static.
- Color scheme, locale, timezone: If these affect the page, set them in the browser context and keep them fixed. They can change text, dates, and layout.
For an archive, use a timestamp that sorts consistently, avoid including untrusted page text in filenames, and decide whether each scheduled run should overwrite a latest image or preserve a history. The example preserves each run.
5. Reliability, performance, and cost
A local scheduled process depends on the machine being on and reachable. A hosted runner depends on that provider’s scheduling and execution environment. These are deployment tradeoffs, not guarantees about a particular provider. Whichever you use, make failures visible and keep the browser version and project dependencies controlled so a later update does not silently change the output.
- Reliability: Use a nonzero process exit code on failure, retain logs, check that the expected file exists, and alert when a run is late or absent. Retries can help with transient failures, but cap them and avoid overlapping jobs unless concurrent runs are intended.
- Performance: Browser startup and page loading usually dominate a small screenshot script. Reuse a browser process only when the scheduler and process design support clean isolation; always close pages and browsers. Full-page images take more memory and storage than viewport captures.
- Consistency: Pin dependencies, use the same browser build and capture parameters, and avoid comparing screenshots taken with different fonts, viewport dimensions, or rendering environments.
- Cost: A local run uses your machine and storage. A hosted run may consume runner time and storage under that provider’s terms. Estimate frequency multiplied by average run duration and retained image size; no provider-specific price or benchmark is asserted here.
- Retention: Apply a retention period, restrict access to output files, and avoid storing images longer than needed. A screenshot can preserve page content that later changes or disappears.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
CAPTURE_URL is missing |
The scheduler does not inherit your interactive shell environment. | Set the variable in the scheduler configuration, or load a protected environment file explicitly. |
| Browser executable not found | Playwright package is installed but Chromium was not installed in that runtime or user environment. | Run npx playwright install chromium in the deployment environment and use the same account for install and execution. |
| Navigation timeout | The target is slow, unreachable, or never reaches the selected lifecycle state. | Confirm the target is permitted and reachable, choose an appropriate navigation wait condition, and set a bounded timeout. Do not work around access controls. |
| Screenshot misses dynamic content | The page rendered its content after DOM readiness. | Wait for a page-specific visible readiness selector or another documented state before capture. |
| Images or fonts appear incomplete | Resources are still loading or are unavailable to the browser. | Wait for the relevant resources or page marker, check the page’s own loading behavior, and keep the environment consistent. |
| Output file is missing | Wrong working directory, unwritable output path, or an earlier exception. | Use an absolute output directory, ensure the scheduler’s account can write there, and inspect the logged error and exit code. |
| Runs overlap or overwrite files | A previous run lasts longer than the interval, or filenames are not unique. | Use unique timestamps, configure the scheduler’s concurrency behavior, and prevent overlap if one run at a time is required. |
| Screenshot differs between runs | Dynamic page content, viewport, browser build, fonts, locale, timezone, or device scale changed. | Fix capture settings and environment; distinguish expected page changes from rendering drift. |
7. Screenshot assertions are a different feature
Playwright Test’s toHaveScreenshot() is an assertion for visual tests. Its documentation says it waits until two consecutive screenshots are the same before comparing the last screenshot with the expectation, and that screenshot assertions work only with Playwright’s test runner. A recurring archive script that simply saves images does not need this assertion. See the PageAssertions documentation.
8. Or skip the browser setup
For a capture workflow you are permitted to run, ScreenshotNeo offers a one-request screenshot API and an MCP server for AI agents. It accepts the consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, with page verdict and billing information in response headers.
One-call cURL example (see the ScreenshotNeo API documentation):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
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)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also provides the MCP tools take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo and its documentation. As with browser automation, use it only for targets and purposes you are permitted to access.
Sign up for 1,000 free screenshots a month, with no card required.
9. FAQ
Does changing the capture time make automated Google Search rank checks allowed?
No. Scheduling or rotating times does not change the permission requirement described by Google’s policy.
Should I use Playwright Test for an image archive?
No. Use page.screenshot() to save each capture. Use screenshot assertions when you are writing visual tests with Playwright Test.
Can I use this script for another website?
Yes, when the access and capture are permitted for that site and purpose. Check the relevant terms and permissions before automating recurring access.


