Give each capture a different destination path. A constant value such as screenshots/page.png tells Puppeteer to write every image to the same file, so each successful call replaces the previous one. Build the filename from a counter, run ID, URL slug, or collision-resistant suffix, create the directory first, and await navigation and screenshots in order.
The basic fix: generate a path inside the loop
Puppeteer’s ScreenshotOptions.path is optional and identifies the output file. Relative paths are resolved from the current working directory, and the image format is inferred from the extension. Move filename construction into the iteration so every call receives a new path.
import puppeteer from 'puppeteer';
import { mkdir } from 'node:fs/promises';
import { join } from 'node:path';
const outputDir = join(process.cwd(), 'screenshots');
await mkdir(outputDir, { recursive: true });
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
const urls = [
'https://example.com/one',
'https://example.com/two',
'https://example.com/three',
];
for (const [index, url] of urls.entries()) {
await page.goto(url, { waitUntil: 'networkidle2' });
const filename = `page-${String(index + 1).padStart(3, '0')}.png`;
await page.screenshot({
path: join(outputDir, filename),
fullPage: true,
});
}
} finally {
await browser.close();
}
This produces page-001.png, page-002.png, and page-003.png. The padded number keeps alphabetic and chronological order aligned. The counter is appropriate when the input order is stable and each run uses a fresh directory.
Why page.screenshot() keeps replacing your image
A path is not a collection name; it is one filesystem destination. If the loop repeatedly passes screenshots/page.png, the operating system opens that same file for writing each time. Node.js documents that its normal asynchronous file write replaces an existing file by default, which is why no Puppeteer error is required for the overwrite to occur.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
Screenshot calls are asynchronous. Always await each call when order matters. Puppeteer coordinates screenshot operations within a browser context, but it cannot infer that two identical paths should be preserved as separate versions.
Choose a naming strategy
| Strategy | Collision resistance | Reproducibility | Sortability | Best use |
|---|---|---|---|---|
| Zero-padded counter | Low across repeated runs | High | High | One run in a new directory |
| Run ID plus counter | High between runs | High within a run | High | Scheduled jobs and archived captures |
| Random suffix | Very high | Low | Moderate | Several workers sharing one directory |
Exclusive create (wx) |
Guaranteed by the write layer | Depends on name generation | Depends on prefix | Never replacing an existing path |
Separate every run with an identifier
If rerunning the script must preserve earlier output, put a timestamp-like identifier in the directory or filename. For example, use screenshots/20260929T125922Z/page-001.png. Remove punctuation from an ISO timestamp so it is safe and easy to sort. A run ID prevents one invocation from replacing another while retaining deterministic numbering inside the run.
Use a random suffix for shared directories
A timestamp alone can collide when concurrent processes start during the same time unit. Add a cryptographically strong or otherwise collision-resistant suffix while retaining a readable prefix, such as home-004-a8f31c.png. This is less reproducible, so record the mapping between input URL and generated name if another system will consume the files.
Guarantee non-replacement with exclusive creation
Puppeteer’s path option writes the capture directly; exclusive creation is a separate Node.js filesystem concern. To reject an existing destination, request the image as a buffer and write it with the wx flag:
Rank #2
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
import { writeFile } from 'node:fs/promises';
async function saveWithoutReplacing(page, path) {
const buffer = await page.screenshot({ fullPage: true });
await writeFile(path, buffer, { flag: 'wx' });
}
If the write fails with EEXIST, generate another name and retry. Do not silently switch back to a normal write, because that restores overwrite behavior.
Capture only what you intend
Viewport versus full page
page.screenshot() normally captures the current viewport. Set fullPage: true when the desired artifact is the entire scrollable document. Full-page mode does not automatically load an infinite-scroll feed: implement scrolling and a stopping condition before capturing if content appears only after scrolling.
A single rendered element
When the requirement is a component rather than a page, locate it and use its ElementHandle.screenshot(). This avoids producing large files containing unrelated content and lets your filename describe the component or test case.
File formats
Use an extension that matches the required output, such as .png or .jpeg; Puppeteer infers the screenshot type from that extension. Keep one extension policy for a batch so downstream processing does not need to guess formats.
Recommended Free Tools
Rank #3
Safe URL-derived filenames
URL text can contain slashes, query delimiters, reserved device names, or characters that are invalid on a target operating system. Never concatenate a raw URL into a path. Extract a short hostname or label, replace anything outside a conservative set such as letters, numbers, dots, underscores, and hyphens, and cap its length. Keep the counter or run ID separate so two URLs with similar labels still receive unique names.
function safeLabel(value) {
return value
.replace(/[^a-z0-9._-]+/gi, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80) || 'page';
}
const host = new URL(url).hostname;
const filename = `${safeLabel(host)}-${String(index + 1).padStart(3, '0')}.png`;
Ordering, concurrency, and reliability
Sequential capture
The loop in the example waits for navigation and then for the screenshot before moving to the next URL. This is simplest, keeps numbering deterministic, and limits simultaneous browser work. Use an appropriate readiness condition—networkidle2 is useful for many pages, but pages with persistent analytics connections may never become truly idle; add a bounded timeout and an explicit selector or delay when the page has a known readiness signal.
Parallel workers
Parallelism can improve throughput, but several workers writing predictable names can race. Give each worker a unique run or worker prefix, or use random names plus exclusive creation. Limit concurrency to the memory and CPU available to Chromium, and preserve the input index in metadata if completion order differs from input order.
Always close Chromium
Put browser.close() in a finally block. A failed navigation, invalid URL, or filesystem exception should not leave Chromium processes running and consuming resources on later jobs.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #4
Operational checklist
- Create the destination with
mkdir(..., { recursive: true }). - Generate a unique filename for every iteration.
- Sanitize any URL-derived label.
- Await
page.goto()andpage.screenshot()in the intended order. - Choose
.png,.jpeg, or another extension deliberately. - Use
fullPage: trueonly for document-length images. - For shared directories, combine unique names with an exclusive-create check.
- Close the browser in
finally.
Troubleshooting common failures
Only one file exists
Cause: every iteration uses the same path. Fix: include the loop index, run ID, or random suffix in the filename and log the resolved path before capturing.
ENOENT or a missing-directory error
Cause: the parent directory does not exist or the process is running from an unexpected working directory. Fix: create it recursively and log process.cwd(); prefer an absolute path built with join().
EEXIST with exclusive writes
Cause: the generated name already exists. Fix: catch the error, add a new suffix, and retry. This is the expected protection provided by wx, not a browser failure.
Screenshots are blank or incomplete
Cause: capture started before the relevant UI rendered, or a lazy/infinite feed was never activated. Fix: wait for a specific selector, an application-ready condition, or a bounded delay; scroll deliberately for infinite content; then capture.
Best Value
- JavaScript Jquery
- Introduces core programming concepts in JavaScript and jQuery
- Uses clear descriptions, inspiring examples, and easy-to-follow diagrams
Navigation times out
Cause: the page has slow resources, a blocked request, or connections that remain open. Fix: set a suitable timeout, choose a less strict readiness condition, and verify the URL independently. Do not treat a timeout as a valid screenshot; record the failure and continue or retry according to your job policy.
Files from simultaneous jobs still collide
Cause: both jobs use the same counter and directory. Fix: isolate directories by run, add worker IDs or random suffixes, and use exclusive creation when replacement is unacceptable.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, with the response identifying the page verdict and billing status. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—let Claude, Cursor, or another MCP client request captures without your own Chromium process.
One request returns an image or PDF. Choose a unique output filename in your own loop while ScreenshotNeo handles rendering:
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchescurl -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}`);
See the ScreenshotNeo documentation for options such as full-page capture, CSS selectors, device presets, custom JavaScript, waits, blocking requests, caching, signed links, asynchronous jobs, webhooks, and bulk capture of up to 100 URLs per call. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Does changing the screenshot format prevent overwriting?
No. The extension selects the format, but uniqueness comes from the complete path. Two calls to the same page.png or page.jpeg destination can still replace one another.
Can I reuse one Puppeteer Page for many URLs?
Yes. Reusing a page is compatible with separate files; navigate, await the desired readiness condition, and pass a new path for each URL.
How should I resume a partially completed batch?
Persist the input index and output path, then skip records whose files pass your validity check. Use run-specific directories so a resumed job cannot overwrite a prior run.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallCrashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteQuick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




