How to Take Website Screenshots with Code: 6 Ways
Take a screenshot of a website programmatically: DevTools, the Chrome CLI, Puppeteer, Playwright, Selenium or an API. Timed runs and code for each.
There are six practical ways to take a screenshot of a website with code. Here is the shortest version of each, all producing a 1280×800 PNG of the same page:
# 1. Chrome's own command line chrome --headless --window-size=1280,800 --screenshot=page.png https://example.com
// 2. Puppeteer (Node) await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com'); await page.screenshot({ path: 'page.png' });
// 3. Playwright (Node, Python, Java, .NET) const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto('https://example.com'); await page.screenshot({ path: 'page.png' });
# 4. Selenium (Python, Java, C#, Ruby, JavaScript) driver.get('https://example.com') driver.save_screenshot('page.png')
# 5. A screenshot API, from any language that speaks HTTP curl -sS --get https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" -d width=1280 -d height=800 -o page.png
The sixth is manual: Chrome DevTools has a screenshot command, which is how you check what the others should produce. This guide compares all six on the same page, with timed runs and complete, tested code, and tells you which one fits your job. The short version: use the command line for one-off captures, a browser library when the screenshot is part of a test or a flow you already automate, and an API when the screenshot is the product.
Tested on 2026-10-18 on an Apple M1 Pro (32 GB), macOS 27.0.1, home broadband. Google Chrome 155.0.8059.80; Chrome for Testing
chrome-headless-shell156.0.8078.4; Node 22.18.0 with[email protected]and[email protected]; Python 3.15.0 withselenium==4.51.0. Linux checks: Debian 12 in Docker with Chromium 155.0.8059.79. Target:https://example.com. We did not time the API, because the result depends on your network and our cache more than on code; see the method notes below.
Which method fits your job
| Your situation | Use | Why |
|---|---|---|
| One screenshot, now, by hand | DevTools | No code. Also the reference image for the rest |
| One screenshot from a script or cron, no dependencies | Chrome CLI | Ships with Chrome. Weak error reporting |
| Node app, need waits, clicks, cookies, element capture | Puppeteer or Playwright | Full browser control |
| Python, Java or .NET app | Playwright, or Selenium if you already have it | Official bindings in each language |
| Screenshot on test failure | Your test framework's browser (Selenium, Playwright) | The browser is already open at the failing step |
| Screenshots are a feature: previews, OG images, archives, reports | An API | No browser fleet to run. Caching and CDN URLs |
| Language with no browser library (Elixir, Rust, shell, Deno) | An API | Plain HTTPS |
Pages on localhost or a private network |
Any local method | A cloud renderer cannot reach your laptop |
The rest of this page goes through each method in that order, then compares them on the same capture.
Method 1: Chrome DevTools, by hand
Open the page in Chrome, open DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I elsewhere), press Cmd+Shift+P or Ctrl+Shift+P, type screenshot and pick one of the four commands: area, full size, node, or (plain) screenshot of the viewport. To get a fixed size, turn on the device toolbar first and type the width and height.
DevTools is not automation, but it belongs here for one reason: it is the fastest way to see what a correct capture of a page looks like, with your fonts, your cookies and your logged-in session. When an automated capture looks wrong, compare it with this one first. If DevTools is wrong too, the problem is the page, not your code. Our full page screenshot in Chrome guide covers the full-size command and its limits.
Method 2: the Chrome command line
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" # macOS # CHROME=google-chrome # Linux "$CHROME" --headless --hide-scrollbars --window-size=1280,800 \ --screenshot=page.png https://example.com
One command, no dependencies, and the file extension picks the format (page.webp and page.jpg both worked). It is also the weakest method for production:
- It exits 0 when the page fails to load. We pointed it at a host that does not exist: no file, exit code 0.
- It has no wait-for-element, no cookies or headers and no full page.
- The full Chrome binary is slow to start.
chrome-headless-shell, a separate download that takes the same flags, was seven times faster in our runs.
Every screenshot and PDF flag, the old and new headless modes, and the Linux sandbox error are in our Chrome headless command line reference. It is the right choice for a cron job that captures your own pages. Wrap it so that "no file" becomes a failure:
rm -f "$out" "$CHROME" --headless --window-size=1280,800 --screenshot="$out" "$url" 2>/dev/null || true [ -s "$out" ] || { echo "no screenshot for $url" >&2; exit 1; }
Method 3: Puppeteer
Puppeteer is Google's Node library for Chrome and Firefox over the DevTools protocol and WebDriver BiDi. It downloads its own Chrome for Testing on install, or uses the Chrome you have:
npm i [email protected]
// screenshot.mjs - node screenshot.mjs https://example.com import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const browser = await puppeteer.launch({ channel: 'chrome' }); // drop channel to use Puppeteer's own Chrome try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto(url, { waitUntil: 'load' }); await page.screenshot({ path: 'puppeteer.png' }); } finally { await browser.close(); }
The finally matters. Without it, an exception in goto leaves a Chrome process running, and a worker that does this once per failed job runs out of memory. An unhandled rejection from top-level await still exits Node with a non-zero status, so failures are not silent.
Puppeteer's strengths for screenshots: type: 'webp' and quality options, clip, fullPage, omitBackground for transparent PNGs, and element handles with their own screenshot(). Our Puppeteer screenshot guide covers each option; the full page Puppeteer guide covers lazy images and very tall pages.
Method 4: Playwright
Playwright drives Chromium, Firefox and WebKit with one API, and has official packages for Node, Python, Java and .NET. Its screenshot API is close to Puppeteer's; the differences are in waiting and in browser contexts:
npm i [email protected] npx playwright install chromium
// screenshot.mjs - node screenshot.mjs https://example.com import { chromium } from 'playwright'; const url = process.argv[2] ?? 'https://example.com'; const browser = await chromium.launch(); try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto(url); await page.screenshot({ path: 'playwright.png' }); } finally { await browser.close(); }
We ran this listing with [email protected] and the headless shell that npx playwright install puts in its cache; the timings below were taken with [email protected], the same library without the install command. chromium.launch() uses that headless shell by default. With { channel: 'chrome' } it uses your installed Chrome instead, and our median time per capture went from 0.91 s to 4.0 s, because the full Chrome binary is much slower to start and stop.
The viewport is set on the page (or the context), not in a separate call, and the device scale factor belongs on browser.newContext({ deviceScaleFactor: 2 }). Playwright's locators wait for elements automatically, which removes a class of "element not found" failures. More in our Playwright screenshot guide, and a TypeScript side-by-side of both libraries and an API in website screenshots in Node.js.
Method 5: Selenium
Selenium is the WebDriver standard's reference implementation, with bindings for Python, Java, C#, Ruby and JavaScript. It is the method you already have if your company runs Selenium tests. Since Selenium 4.6, Selenium Manager finds the driver binary, so setup is one package:
pip install selenium==4.51.0
from selenium import webdriver options = webdriver.ChromeOptions() options.add_argument('--headless=new') driver = webdriver.Chrome(options=options) try: driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { 'width': 1280, 'height': 800, 'deviceScaleFactor': 1, 'mobile': False, }) driver.get('https://example.com') driver.save_screenshot('selenium.png') finally: driver.quit()
The device metrics call is not decoration. Selenium sizes the window, not the viewport, and headless Chrome keeps 143 px of that window for browser UI it never draws: set_window_size(1280, 800) gave a 1280×657 image in our test. Selenium also has no format option (PNG only), and full-page capture works natively only in Firefox. The Selenium screenshots guide has tested programs in Python, Java and C#; the Python guide adds a pytest hook that saves a screenshot when a test fails.
Method 6: a screenshot API
An API moves the browser to someone else's servers. You send the URL and options; you get the image back. Because it is plain HTTPS, it works from any language, which is why this section shows the wire format first and an SDK second.
The request, for any language
A GET request with flat query parameters returns the image as the response body:
curl -sS --fail-with-body --get https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ --data-urlencode "url=https://example.com" \ -d width=1280 -d height=800 -d format=webp \ -o page.webp -D headers.txt
A POST request takes a nested JSON body and supports every option, such as waiting for a selector or capturing one element:
curl -sS --fail-with-body https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com", "viewport": { "width": 1280, "height": 800 }, "wait": { "until": "load", "for_selector": "p", "timeout": 15 }, "output": { "format": "webp", "quality": 80 } }' \ -o page.webp -D headers.txt
To call this from any language, your HTTP client needs to do four things:
| Step | Detail |
|---|---|
| Authenticate | Authorization: Bearer <key> header. Keys start with rs_live_ |
| Send options | GET: URL-encoded query string. POST: JSON body with Content-Type: application/json |
| Read the result | Status 200: the body is the image, with Content-Type set to match |
| Read errors | Status 4xx or 5xx: a JSON body with error.code, error.message and error.retryable |
Set the client timeout to at least 60 seconds, because a capture can wait up to 60 seconds for the page. Encode the target URL as a value; an unencoded & or # in it silently cuts the target short. The GET endpoint reference lists every flat parameter, and using the API without an SDK has the same POST in Node, Python, Ruby, PHP, Go and Java with standard-library HTTP only. The Selenium guide has a C# version.
The response headers carry more than the image. X-Cache-URL is a public CDN link to the same file, which you can store or embed without your key. X-Screenshot-Title, X-Screenshot-Description and X-Screenshot-Favicon carry the page's real title, meta description and favicon, which saves a second fetch if you are building link previews.
The same request with an SDK
Official SDKs exist for Node, Python, Ruby, PHP, Go and Java. In Node:
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.url('https://example.com').width(1280).height(800).format('webp') ); await writeFile('page.webp', image);
The SDK index links each one, and language guides such as website screenshots in Ruby set the SDK against the local browser gems. The trade-offs are real: you pay per capture after the 150 free credits each month, the renderer cannot reach localhost or your private network, and you depend on a vendor. In return you do not run Chrome, fonts, sandboxes or a browser pool.
Measured: the same capture six ways
Each method captured https://example.com at 1280×800, PNG, device scale factor 1. The time is wall-clock time for one complete run of the tested script or command, so it includes starting the runtime and the browser and shutting them down. That is what a one-off job or a shell loop pays per page. Each number is the median of 6 runs.
| Method | Browser | Median | Range | PNG bytes | Lines in our listing |
|---|---|---|---|---|---|
| Chrome CLI | chrome-headless-shell 156 |
0.78 s | 0.55 to 0.91 s | 79,353 | 1 |
| Playwright 1.64.1 (Node) | Its headless shell | 0.91 s | 0.74 to 1.82 s | 80,922 | 10 |
| Puppeteer 25.14.0 (Node) | Google Chrome 155 | 1.18 s | 1.13 to 1.38 s | 81,121 | 11 |
| Selenium 4.51.0 (Python) | Google Chrome 155 | 2.19 s | 1.97 to 2.77 s | 81,121 | 40 |
| Playwright 1.64.1 (Node) | Google Chrome 155 (channel: 'chrome') |
4.0 s | 3.62 to 4.73 s | 80,629 | 10 |
| Chrome CLI | Google Chrome 155 | 5.67 s | 4.10 to 9.35 s | 81,121 | 1 |
| DevTools | Google Chrome 155 | manual | n/a | n/a | 0 |
| API | Remote | not timed | n/a | n/a | 1 (curl) |
Method notes. One machine, one page, 2026-10-18. Example.com is tiny, so these numbers measure tool and browser overhead, not rendering. The Selenium listing is the 40-line version with the viewport helper from the Python guide; Selenium Manager also checked for driver updates on every start, and the network check failed and fell back to its cache, which adds time. We ran Puppeteer against the installed Chrome to avoid a second download; with Puppeteer pointed at the headless shell, three runs took 0.61 to 1.13 s. Lines count the complete tested file, including imports and blank lines.
Three things stand out:
- Browser start-up dominates. With the browser already running, Selenium loaded and captured the same page in a median 0.067 s. If you take many screenshots, the method matters less than whether you keep a browser open between them.
- The headless shell is the fast path for every library that can use it, because it skips the full browser's profile and UI setup.
- Bytes match within 3%. The image is the same; the engine is the same. Format choice changes size far more: the same capture was 36,008 bytes as WebP and 59,166 as JPEG from the Chrome CLI.
We left the API out of the timing table on purpose. Its time is a network round trip plus a render on our side, and a repeat request for the same options can come from cache. A fair test runs from the server where your jobs run, against your own URLs.
What each method costs to run
Lines of code are the small cost. The large ones show up later:
| Cost | CLI | Puppeteer / Playwright / Selenium | API |
|---|---|---|---|
| Install | Chrome | Library plus a browser (Selenium Manager or npx playwright install) |
HTTP client |
| Linux server | Chrome's libraries, fonts, sandbox decision | Same, plus Node, Python or a JVM | None |
| Memory per open browser | Hundreds of MB | Hundreds of MB; we measured about 360 MB per leaked Selenium session | None on your side |
| Concurrency | One process per capture | Browser pool you build | Account rate limit shared by all your keys |
| Failure handling | Check the file yourself | Exceptions, timeouts you set | HTTP status and error codes |
| Money | Server time | Server time | Per capture after the free credits |
For the server-side costs in money, our API vs self-hosted cost breakdown works through the numbers.
Problems you will hit with every method
These are not library bugs. They come from Chrome, Linux and the pages themselves, so they follow you from one method to the next. Each is reproduced with a fix in headless Chrome production failures.
| Symptom | Cause | Fix |
|---|---|---|
| Image is shorter than the size you set | Selenium sets the window, not the viewport | Set the viewport (setViewport, viewport, or CDP device metrics) |
| Screenshot is dark on your laptop, light on the server | Headless Chrome follows the OS colour scheme | Force prefers-color-scheme (--blink-settings=preferredColorScheme=1, colorScheme: 'light' in Playwright) |
| CJK text or emoji show as boxes on Linux | No font with those glyphs | apt-get install fonts-noto-cjk fonts-noto-color-emoji |
Running as root without --no-sandbox is not supported |
Chrome in a container as root | Non-root user, or --no-sandbox in an isolated container |
| Script hangs for minutes | A request never finishes, so load never fires |
Short navigation timeout, then wait for the element you need |
Zombie chromium processes in ps |
No init process reaps orphans in the container | docker run --init |
| Memory grows until the worker dies | Browsers not closed after errors | Close in finally; restart long-lived browsers on a schedule |
| CLI exits 0 but no file | Chrome logs load failures without an exit code | Check that the file exists and is not empty |
| Blank areas where images should be | Lazy images below the fold never loaded | Scroll first, or use a full-page mode that does; see lazy-loaded images |
| Cookie banner covers the page | Consent pop-up on first visit | Hide it with CSS, set the consent cookie, or use a blocker |
Which one should you choose?
- You need one image today: DevTools or the CLI.
- You run a nightly job over your own 50 pages: the CLI with
chrome-headless-shelland the file check, or Playwright if you need waits. - The screenshot is a step in a test: the browser library your tests already use. Selenium users: see the Selenium guide. Node users choosing between the two: Puppeteer vs Playwright for screenshots.
- The screenshot is what your product delivers, or you need it from a language with no browser library: an API.
You can try the API side of that decision with the same curl command from the top of this page. Create a free account, set RS_API_KEY, and compare the result with the image from your own browser code.
Related in this topic
- Selenium Screenshots in Python, Java and C# (Selenium 4) Take a screenshot with Selenium 4 in Python, Java and C#: page, element, exact viewport and PDF. Same test in three languages, timed, with the fixes.
- Playwright Screenshots: Tested Recipes and Pitfalls Take screenshots with Playwright: page.screenshot options, deviceScaleFactor, locators, masks, measured format sizes, auth, Linux fonts and production setup.
- Puppeteer Screenshots: Tested Recipes for Puppeteer 25 Every Puppeteer screenshot recipe re-run on Puppeteer 25.14 and Chrome 155: viewport, full page, element, clip, waits, formats, auth, blocking and fonts.
- Website Screenshots in Node.js: Puppeteer, Playwright, API Take website screenshots in Node.js 22 with TypeScript and ESM: Puppeteer, Playwright or one fetch call to an API, with measured install cost and tested code.
- Website Screenshots in Python: Selenium, Playwright, API Capture a website as an image in Python with Selenium, Playwright or an API: measured setup and per-shot time, async batches, and fixes for common failures.
- Website Screenshots in Ruby: Ferrum, Grover or an API Website screenshots in Ruby with Ferrum, Grover or an API: tested code for viewport, full page, dark mode and cookies, measured file sizes, troubleshooting.