How to Capture a Puppeteer Screenshot Every 10 Seconds
Use a serial Puppeteer loop to capture a page every 10 seconds, with reliable waits, filenames, scheduling, cleanup, and troubleshooting.
Use page.screenshot() inside an awaited loop. Take one screenshot, wait for it to finish, wait ten seconds, then capture again. This serial design prevents overlapping captures. The real start-to-start interval is ten seconds plus navigation, rendering, screenshot, and file-write time.
import puppeteer from 'puppeteer';
import fs from 'node:fs/promises';
const url = 'https://example.com';
const intervalMs = 10_000;
const outputDir = './screenshots';
await fs.mkdir(outputDir, { recursive: true });
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'networkidle2' });
let sequence = 0;
while (true) {
const timestamp = new Date().toISOString().replaceAll(':', '-');
const path = `${outputDir}/capture-${timestamp}-${sequence++}.png`;
await page.screenshot({ path, fullPage: true });
console.log(`Saved ${path}`);
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
} finally {
await browser.close();
}
Puppeteer’s screenshot API is Page.screenshot(), and supplying path writes the image to disk. The API can also return image bytes when no path is supplied, or a base64 string when an encoding is requested. See the official screenshot guide.
1. Set up the project
- Install a current Node.js release.
- Create a project and install Puppeteer:
mkdir puppeteer-captures
cd puppeteer-captures
npm init -y
npm install puppeteer
mkdir screenshots
Save the script as capture.mjs and run it with node capture.mjs. Stop it with Ctrl+C. The finally block closes the browser when the process exits through an exception or a controlled stop.
2. Understand what “every 10 seconds” means
The basic loop waits ten seconds after each completed screenshot:
await page.screenshot({ path });
await new Promise(resolve => setTimeout(resolve, 10_000));
If capture and writing take 1.5 seconds, starts are about 11.5 seconds apart. This is usually the safest policy because only one capture is active at a time.
Fixed start times with missed-slot handling
If captures must target 00:00, 00:10, 00:20, and so on, calculate deadlines from a start time. When a capture takes longer than ten seconds, this version skips missed slots instead of launching concurrent screenshots:
const period = 10_000;
const startedAt = Date.now();
let slot = 0;
while (true) {
const deadline = startedAt + slot * period;
const delay = deadline - Date.now();
if (delay > 0) await new Promise(resolve => setTimeout(resolve, delay));
const timestamp = new Date().toISOString().replaceAll(':', '-');
await page.screenshot({ path: `${outputDir}/fixed-${timestamp}-${slot}.png` });
slot++;
while (startedAt + slot * period <= Date.now()) slot++;
}
Launching concurrent screenshot calls by default can overload the page, reorder files, and increase memory use. Choose concurrency only when missed slots matter more than serialized output.
3. Wait for the page to be ready
waitUntil: 'networkidle2' is a useful starting point, but it is not universal. Pages with polling, analytics, streaming, advertisements, or long-lived connections may never become truly idle or may become idle before the important content appears.
Wait for a selector
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-testid="dashboard"]', { timeout: 30_000 });
await page.screenshot({ path });
Wait for a function or a bounded delay
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => document.querySelectorAll('.chart').length > 0, {
timeout: 30_000
});
await new Promise(resolve => setTimeout(resolve, 1_000));
Prefer a page-specific selector or condition. A delay alone is less reliable because network and rendering time vary. Puppeteer documents navigation waits, function waits, and selector waits in its API documentation.
4. Control the capture area and appearance
| Goal | Option | Example |
|---|---|---|
| Viewport consistency | Set it before navigation | page.setViewport({ width: 1280, height: 800, deviceScaleFactor: 2 }) |
| Entire document | fullPage |
page.screenshot({ fullPage: true }) |
| Specific element | Element handle screenshot | await page.locator('.price').screenshot({ path }) |
| Format | PNG, JPEG, or WebP options | { type: 'jpeg', quality: 80 } |
| Transparency | Omit a page background where supported | { omitBackground: true } |
Use a fixed viewport and device scale factor when comparing frames over time. Responsive breakpoints, font loading, animations, timestamps, and rotating content can otherwise create differences unrelated to the change you are monitoring.
Capture one element
const chart = await page.$('#sales-chart');
if (!chart) throw new Error('Sales chart was not found');
await chart.screenshot({ path });
Element screenshots are useful for dashboards and regression checks. Full-page screenshots include the whole document and can be tall enough to consume substantially more memory.
5. Stop cleanly and run for a defined duration
An infinite loop is convenient during development. A service should have a stop condition or respond to an abort signal:
const controller = new AbortController();
process.on('SIGINT', () => controller.abort());
process.on('SIGTERM', () => controller.abort());
while (!controller.signal.aborted) {
const timestamp = new Date().toISOString().replaceAll(':', '-');
await page.screenshot({ path: `${outputDir}/capture-${timestamp}.png` });
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
For a finite run, replace while (true) with a counter or an end time. Keep browser creation outside the loop so Chrome is not relaunched every ten seconds.
6. Browser modes and debugging
Puppeteer launches headless Chrome by default. Use headless: false to watch the page while diagnosing selectors, redirects, or layout problems:
const browser = await puppeteer.launch({ headless: false, slowMo: 100 });
The current launch API also documents headless: 'shell'. Chrome Headless Shell can be faster for some automation workloads, but it does not completely match regular Chrome behavior. Puppeteer guarantees compatibility with its bundled browser; using an alternate executable is your responsibility. See the launch options documentation.
7. Authentication, cookies, and dynamic pages
Open a page, establish its session, and then enter the capture loop:
const page = await browser.newPage();
await page.setCookie({
name: 'session',
value: process.env.SESSION_COOKIE,
domain: 'example.com',
path: '/'
});
await page.goto('https://example.com/dashboard', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#dashboard');
For basic HTTP authentication, use page.authenticate() before navigation. For application login, automate the form once, verify the post-login selector, and reuse that page. Never hard-code credentials in the script or commit them to source control.
8. Reliability and performance checklist
- Reuse one browser and page for a recurring job.
- Await every navigation, wait, and screenshot.
- Use a selector or function that represents real readiness.
- Set explicit navigation and selector timeouts.
- Write unique filenames containing a timestamp and sequence number.
- Keep screenshots on a filesystem with enough free space; rotate or upload old files.
- Limit full-page and high device-scale captures when memory is constrained.
- Prevent overlapping work unless concurrency is deliberate and bounded.
- Record URL, slot, duration, and error details for each attempt.
- Restart a failed page or browser under a supervisor after repeated failures.
Screenshot time depends on page size, scripts, fonts, network resources, and encoding. Puppeteer does not promise a ten-second real-time clock. If the page is slow, measure navigation and screenshot durations separately and decide whether to skip late slots or capture immediately after the previous frame.
9. Common errors and fixes
| Error | Likely cause | Fix |
|---|---|---|
TimeoutError: Navigation timeout |
The page did not satisfy the navigation wait in time. | Use a suitable waitUntil, raise the timeout for this page, or wait for a specific selector after domcontentloaded. |
| Selector wait times out | The selector is wrong, content is behind login, or rendering failed. | Run headed with slowMo, inspect the DOM, verify authentication, and check console/network errors. |
| Blank or partial screenshot | Capture happened before fonts, images, or client rendering completed. | Wait for a meaningful selector, document.fonts.ready, image completion, or a short bounded delay. |
| Files overwrite each other | The filename is constant or timestamps have insufficient precision. | Include ISO time plus an incrementing sequence number. |
| Out-of-memory or very large files | Repeated full-page or high-scale captures. | Capture an element or viewport, lower deviceScaleFactor, use JPEG/WebP where suitable, and rotate files. |
| Browser fails to launch in a container | Missing bundled dependencies or sandbox restrictions. | Install the dependencies required by the bundled browser, use the documented container setup, and inspect launch stderr before changing flags. |
| Captures drift from ten-second boundaries | Capture and write time is added to the sleep interval. | Use the fixed-deadline loop and explicitly choose a skip policy for missed slots. |
10. Or skip the browser setup
If you only need a clean image on a schedule, ScreenshotNeo provides a single HTTP request instead of maintaining Chrome. Its cookie and consent step accepts the banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. It also offers an MCP server for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options, including viewport and device presets, full-page or CSS-element capture, dark mode, retina scale, custom CSS and JavaScript, waits, blocking rules, headers, cookies, user agents, geolocation, caching, signed links, asynchronous jobs, bulk capture, PDF, and usage reporting.
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}`);
ScreenshotNeo includes 1,000 shots per month free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
11. FAQ
Does Puppeteer take the first screenshot immediately?
Yes. In the serial example, navigation and readiness happen first, then the first screenshot is saved, followed by the ten-second wait.
Can I use setInterval?
You can, but callbacks may overlap when a capture takes longer than the interval. An awaited loop makes overlap behavior explicit.
Should I reload the page every ten seconds?
Only when the purpose is to observe a fresh navigation state. For a live dashboard, keeping one page open may preserve state; reloading can add load time and trigger login or consent flows again.
How do I compare frames?
Keep viewport, device scale, fonts, timezone, and readiness conditions stable, then compare files with an image-diff tool. Exclude intentional timestamps, rotating ads, and animations when possible.
Can Puppeteer capture PDFs on the same schedule?
Yes. Use page.pdf() instead of page.screenshot() when the output is a PDF, while retaining the same scheduling and cleanup patterns.


