Use Playwright’s asynchronous Python API to open each URL in a fresh page, save a screenshot, and close the page—even when an individual navigation fails. The example below bounds concurrent pages, creates a separate PNG for each URL, and closes the browser context and browser when the batch is done. Use full_page=True to capture the whole scrollable document instead of only the visible viewport.
Install Playwright and its browser
Install the Python package and Chromium before running the script:
As an Amazon Associate I earn from qualifying purchases.
python -m pip install playwrightpython -m playwright install chromium
The example uses Python’s asyncio API. Playwright provides both synchronous and asynchronous Python APIs; async is a natural fit when the rest of your application already uses asyncio. See the Playwright Python getting-started documentation.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Run a batch with bounded concurrency
Save this as bulk_screenshots.py. It writes one full-page PNG per URL in the screenshots directory. The four-page limit is an example, not a recommended maximum: adjust it for your machine, the pages you capture, and the load appropriate for target sites.
#1 Best Overall
import asyncio
from pathlib import Path
from playwright.async_api import async_playwright
URLS = [
"https://example.com/",
"https://playwright.dev/python/",
]
OUT = Path("screenshots")
MAX_CONCURRENT_PAGES = 4 # Example only; tune for your workload.
async def main():
OUT.mkdir(parents=True, exist_ok=True)
semaphore = asyncio.Semaphore(MAX_CONCURRENT_PAGES)
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context(
viewport={"width": 1440, "height": 1000}
)
async def capture(index, url):
async with semaphore:
page = await context.new_page()
try:
response = await page.goto(
url, wait_until="load", timeout=30_000
)
# A response may be absent for some non-HTTP navigations.
status = response.status if response else None
output_path = OUT / f"{index:04d}.png"
await page.screenshot(
path=str(output_path), full_page=True
)
return {
"url": url,
"status": status,
"file": str(output_path),
}
except Exception as exc:
return {"url": url, "error": str(exc)}
finally:
await page.close()
try:
results = await asyncio.gather(
*(capture(index, url)
for index, url in enumerate(URLS, start=1))
)
finally:
await context.close()
await browser.close()
for result in results:
print(result)
asyncio.run(main())
Run it with python bulk_screenshots.py. The output names are based on the URL’s position in the input list, not on raw URL text, so characters such as slashes and query strings cannot accidentally become part of a path. If you run the same list again, matching filenames are overwritten; use a run-specific directory or stable unique identifiers if you need to retain earlier batches.
Choose what the screenshot should include
Viewport or full page
The script sets full_page=True, which captures the complete scrollable page as one image. Set it to False or omit it for a screenshot limited to the viewport configured when the browser context was created. A full-page image can be very tall; choose viewport capture if the consumer expects a screen-sized image. Playwright documents the screenshot options.
Wait for the right page state
wait_until="load" waits for the page load event, but it does not guarantee that client-side rendering, delayed content, or lazy-loaded images are finished. When a site needs additional time or a known element, wait for that condition before taking the screenshot—for example:
Rank #2
await page.goto(url, wait_until="load", timeout=30_000)
await page.locator("main article").wait_for(state="visible", timeout=10_000)
await page.screenshot(path=str(output_path), full_page=True)
Replace main article with a selector meaningful for the target page. Avoid adding a fixed delay to every capture unless the site requires it; a page-specific readiness check is usually more targeted.
Save to a file or capture bytes
For a straightforward batch, pass path to page.screenshot() as the main script does. If the next step is image processing or uploading rather than local file output, omit path and use the returned bytes:
image_bytes = await page.screenshot(full_page=True)
Playwright’s screenshot API supports both saving to a path and returning image bytes.
Rank #3
Choose how to manage pages and browser state
Reuse a context for consistent settings
The sample creates one browser context and a fresh page for each URL. Pages in the same context share context-level settings, such as viewport emulation. This is useful when every capture should use the same browser configuration. Close each page after capture, then close the context and browser; the try/finally blocks ensure cleanup even if a capture raises an exception. See Playwright’s documentation on multiple pages and browser contexts, as well as its browser lifecycle API.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Use separate contexts when sessions must be isolated
A browser context represents an isolated browser session. If URLs must not share session state, cookies, or other context-level configuration, create a separate context for the relevant captures and close it when finished. This uses more browser resources than sharing one context, so choose isolation to meet a real requirement rather than creating a new context automatically for every URL.
Tune concurrency, failures, and output for your batch
Set concurrency deliberately
The semaphore limits how many captures enter the page-creation and navigation section at once. More parallel work may reduce elapsed time for some workloads, but it also uses more local resources and sends more simultaneous requests to target sites. Start conservatively and adjust based on observed memory use, failures, and acceptable site load. Playwright does not publish a universal best number of concurrent pages or a throughput guarantee for this workflow.
Rank #4
Keep errors tied to individual URLs
Each capture catches its own exception and returns an error record, allowing other URLs to finish. The navigation response status is recorded when available, but an HTTP response such as a 404 is not itself a navigation exception; inspect the returned status if your workflow should treat non-success responses as failures. A None status means no response object was returned, not that the page necessarily succeeded.
Make names stable when needed
The index-based names are unique within this run and easy to sanitize, but reordering the input changes which URL receives each number. If reproducibility matters, assign each URL a stable identifier and sanitize it before using it in a filename. Do not use an untrusted raw URL as a filesystem path.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsUse a synchronous script for a small one-off batch
If your script is not part of an async application, Playwright’s synchronous API can be simpler to read. The core sequence is the same: start Playwright, launch a browser, create a context and page, navigate, capture, and close resources.
Best Value
from pathlib import Path
from playwright.sync_api import sync_playwright
urls = ["https://example.com/", "https://playwright.dev/python/"]
out = Path("screenshots")
out.mkdir(parents=True, exist_ok=True)
with sync_playwright() as p:
browser = p.chromium.launch()
context = browser.new_context(viewport={"width": 1440, "height": 1000})
try:
for index, url in enumerate(urls, start=1):
page = context.new_page()
try:
response = page.goto(url, wait_until="load", timeout=30_000)
status = response.status if response else None
path = out / f"{index:04d}.png"
page.screenshot(path=str(path), full_page=True)
print({"url": url, "status": status, "file": str(path)})
except Exception as exc:
print({"url": url, "error": str(exc)})
finally:
page.close()
finally:
context.close()
browser.close()
This version processes URLs sequentially. Use the async pattern when you want bounded overlapping work or need to integrate with an asyncio application; async syntax alone does not establish a particular throughput advantage.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Troubleshoot common failures
- Browser executable missing: install Chromium with
python -m playwright install chromiumafter installing the package. - Navigation timeout: the site may be slow, unreachable, or waiting on resources that never finish. Check the URL and network access, then consider a longer per-site timeout or a different readiness condition such as
domcontentloadedplus a selector wait. Do not increase every timeout blindly. - Screenshot misses content: the page may render important content after the load event. Wait for a relevant visible selector or another application-specific condition before capturing.
- One URL disrupts a batch: keep exception handling inside the per-URL capture function, as shown, so failures are returned alongside successful results.
- Images are overwritten: the sample reuses numbered filenames. Use a unique output directory per run or stable identifiers if previous results must be preserved.
- High memory use or excessive target load: lower
MAX_CONCURRENT_PAGES. The right limit depends on the pages, host machine, and target sites.
Or skip the browser setup
ScreenshotNeo offers a hosted screenshot API; its website screenshot API can return an image or PDF from one GET request. For a PNG, JPEG, or WebP response, save the returned bytes to a file. The example follows the API’s documented usage; see the ScreenshotNeo documentation.
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
ScreenshotNeo removes supported cookie and consent banners, newsletter popups, and chat widgets before capture; those cleanup steps can be turned off. Bot checks, 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 screenshot tools for AI agents and MCP clients. The Free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month—no card required.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Frequently Asked Questions
Does the script capture one screenshot per URL?
Yes. Each URL gets a fresh page and a distinct numbered PNG in the output directory.
Can I capture PDF files instead of images with Playwright?
This example uses Playwright’s screenshot API and writes PNG images; PDF output is a separate browser workflow and is not shown here.
Quick 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.




