Recommended Free Tools
To take bulk screenshots with Puppeteer, launch one browser, process a list of URLs, set a consistent viewport, save each page to a unique file, and handle errors per URL so one failure does not end the batch. Start sequentially for predictable resource use; add bounded parallelism only after measuring your own pages and machine.
Build a reliable bulk screenshot workflow
Puppeteer’s documented capture method is Page.screenshot(). For a batch, apply the usual browser workflow to each input URL: create or reuse a page, navigate, capture, and close or reset the page. The code below is a runnable Node.js example that reads URLs from a text file, saves screenshots in a dedicated directory, and records failures without stopping the remaining captures.
Install Puppeteer in a Node.js project with npm install puppeteer. Puppeteer normally downloads a compatible Chrome for Testing browser during installation. Create urls.txt with one URL per line, then save this script as bulk-screenshots.js.
const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');
const INPUT_FILE = path.resolve('urls.txt');
const OUTPUT_DIR = path.resolve('screenshots');
const WIDTH = 1365;
const HEIGHT = 900;
function safeFilePart(value) {
return value.replace(/[^a-z0-9_-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 70) || 'page';
}
async function main() {
const urls = (await fs.readFile(INPUT_FILE, 'utf8'))
.split(/r?n/)
.map(line => line.trim())
.filter(line => line && !line.startsWith('#'));
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const browser = await puppeteer.launch({ headless: true });
const results = [];
try {
for (const [index, url] of urls.entries()) {
const number = String(index + 1).padStart(4, '0');
let page;
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(`Unsupported URL scheme: ${parsed.protocol}`);
}
page = await browser.newPage();
await page.setViewport({ width: WIDTH, height: HEIGHT });
const response = await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 45000
});
const status = response ? response.status() : null;
const filename = `${number}-${safeFilePart(parsed.hostname)}.png`;
await page.screenshot({
path: path.join(OUTPUT_DIR, filename),
fullPage: true
});
results.push({ url, ok: true, status, file: filename });
console.log(`OK ${url} -> ${filename}${status ? ` (HTTP ${status})` : ''}`);
} catch (error) {
results.push({ url, ok: false, error: error.message });
console.error(`FAILED ${url}: ${error.message}`);
} finally {
if (page) await page.close().catch(() => {});
}
}
} finally {
await browser.close();
}
await fs.writeFile(path.join(OUTPUT_DIR, 'results.json'), JSON.stringify(results, null, 2));
const failed = results.filter(result => !result.ok).length;
console.log(`Finished ${results.length} URL(s); ${failed} failed.`);
if (failed) process.exitCode = 1;
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with node bulk-screenshots.js. Each successful capture gets its own numbered filename, and screenshots/results.json contains a success or failure record for every input line. A non-zero process exit code signals that at least one URL failed, which makes the script usable in a scheduled job or CI pipeline without losing the individual results.
#1 Best Overall
Why one browser and a fresh page per URL?
One browser avoids repeatedly launching the browser process, while a fresh page per URL limits state leakage such as cookies, local storage, or a prior page’s JavaScript. Puppeteer allows multiple pages in one browser, but its documentation does not prescribe a universal pool design or safe concurrency count. This example deliberately runs one page at a time, then closes it in a finally block. Browser shutdown is also protected by finally, so a navigation or screenshot error does not bypass cleanup.
The filename combines an input-order number with a sanitized host name. The number prevents collisions when two URLs share a host; it also makes reruns deterministic as long as the input order stays fixed. Avoid using a raw URL or page title as a path: URL characters can be invalid in filenames, titles can be duplicated, and untrusted input should not determine arbitrary filesystem paths.
Choose capture size, format, and readiness
Viewport capture or full page
By default, fullPage is false, so the screenshot covers the visible viewport. Set fullPage: true when the output should include the document’s full height. For a targeted rectangle, use the clip option; use ElementHandle.screenshot() instead when the intended output is a specific element. Element screenshots scroll the element into view when needed and fail if the element has become detached.
Rank #2
Full-page capture can produce very tall images and may expose page behaviors that do not appear in a viewport capture. Some sites lazy-load images as the visitor scrolls. Puppeteer’s screenshot option requests a full-page image, but if those images must be present, verify the page’s own lazy-loading behavior and wait for the relevant content before capture.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesPNG, JPEG, and WebP
The screenshot type defaults to PNG. PNG is lossless and often suitable for text, interface details, and archival comparisons. JPEG or WebP can reduce file size when some image compression is acceptable; the quality option applies to lossy formats, not PNG. Puppeteer’s screenshot options document the supported output behavior. For transparency, set omitBackground: true; otherwise the page background is included.
await page.screenshot({
path: 'screenshots/home.webp',
type: 'webp',
quality: 85,
fullPage: true
});
Use one format and setting consistently across a comparison batch. Changing format, viewport, or device scale can change file dimensions and visual appearance, making before-and-after comparisons less meaningful.
Set viewport before navigation
Set the viewport before calling page.goto(). Puppeteer notes that sites can react to viewport changes, and configuring it before navigation helps keep responsive layout behavior consistent. Use the same width and height for a desktop batch; when capturing mobile layouts, define a separate viewport profile rather than mixing dimensions in one run.
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
Puppeteer supports viewport emulation and device presets. Emulation affects the browser’s reported viewport and related page behavior; it is not a guarantee that every site will look exactly like a physical device.
Wait for the page you actually need
The example uses waitUntil: 'networkidle2' as a starting condition, not proof that a page is visually complete. Puppeteer’s screenshot guide demonstrates that navigation option, but the correct readiness signal depends on the site. Analytics, polling, and long-running connections may prevent network idleness, while an application can become visually ready before all network activity ends.
Rank #4
For a known page, wait for an application-specific selector or signal:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45000 });
await page.waitForSelector('[data-screenshot-ready="true"]', { timeout: 15000 });
await page.screenshot({ path: outputPath, fullPage: true });
If no selector is available, use an intentional short delay only when you understand the page’s rendering behavior. A fixed delay is simple but can waste time on quick pages and still be too short for slow ones. Avoid treating one wait condition as universal.
Scale the batch without making it fragile
Start sequentially, then bound parallel work
Sequential processing is slower than concurrent capture when URLs are independent, but it has a smaller and more predictable load on memory, CPU, browser processes, and target websites. There is no official universal concurrency limit for Puppeteer screenshots. The right number depends on page complexity, output height, available memory, and how heavily each destination site loads.
Best Value
- Used Book in Good Condition
After the sequential run is stable, introduce a small worker pool or semaphore and increase it gradually while watching memory use, timeout rates, browser crashes, and destination-site responses. Do not launch one page per URL all at once for a large list. Keep an upper bound, retain per-URL error handling, and close every page after its capture. For very large jobs, split the input into manageable batches and persist results so a failed run can resume selectively.
Retries and partial results
Retry transient failures such as a timeout or temporary network error, but do not blindly retry every failure. A persistent HTTP error, invalid URL, or selector mismatch usually needs investigation rather than repeated attempts. Store the URL, attempt number, error message, response status when available, and output filename in the result record. Preserve successful screenshots instead of discarding the entire batch when one destination fails.
Choose a retry cap and backoff appropriate to your job, and avoid retrying quickly enough to overload a site. If screenshots are intended for a public website, check that your crawl and automation comply with the site’s terms and applicable access rules.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshooting common failures
- Navigation timeout: the page did not reach the chosen readiness condition before the timeout. Check whether it is slow, continually active, or blocked; try a more suitable lifecycle event and then wait for a meaningful selector. A longer timeout alone may only delay the same failure.
- Screenshot is blank or incomplete: the page may still be rendering, require a consent interaction, or load content after scrolling. Wait for the relevant selector or app signal and inspect the page state before capture. Network idleness does not establish visual readiness for every site.
- One URL stops the whole batch: make sure navigation and capture errors are caught inside the per-URL loop, while page and browser cleanup remain in
finallyblocks. This lets later URLs run and leaves a failure record for the problematic input. - Files overwrite each other: the output naming scheme is not unique. Include a stable index or identifier in every path; hostnames or titles alone can repeat.
- Browser processes remain after an error: ensure
browser.close()is in an outerfinallyblock and each page is closed in an inner one. Avoid forcibly terminating the process as the normal cleanup path. - Full-page image omits expected content: determine whether that content is lazy-loaded or inserted asynchronously. Trigger the site’s relevant loading behavior and wait for a content-specific signal before screenshotting.
- Element capture reports a detached node: the page replaced or removed the element between lookup and capture. Re-query it after the page reaches its ready state, then call its screenshot method.
Or skip the browser setup
If you need a screenshot API rather than maintaining a browser batch, ScreenshotNeo takes a URL in one GET request and returns an image or PDF. It accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.
Crashes, 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 minuteWindows 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 reinstallInstall the required Python package with pip install requests, then use the API key from your account. See the ScreenshotNeo documentation for request options and supported parameters.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
For a shell call, the equivalent cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Other options include full-page and element capture, device and viewport settings, custom CSS or JavaScript, selector waits, request blocking, caching, asynchronous jobs, bulk capture, and PDF settings. Prices are $5 for 3,000 shots on Starter, $15 for 15,000 on Growth, $39 for 60,000 on Pro, $99 for 250,000 on Scale, and $249 for 1,000,000 on Business; yearly billing gives two months free. Every feature is available on every plan. Sign up free for 1,000 screenshots a month with no card.
Quick Recap
Official Puppeteer references
- Screenshots guide
- ScreenshotOptions API
- Page.setViewport API
- Browser.newPage API
- Browser.pages API
- ElementHandle.screenshot API
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.




