Selenium Full Page Screenshot: Firefox, Chrome, Stitching
Take a full page screenshot in Selenium: Firefox's native call, Chrome via CDP Page.captureScreenshot, or scroll and stitch. Measured limits, times and code.
Selenium has a full page screenshot command only for Firefox:
import sys from selenium import webdriver url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com' options = webdriver.FirefoxOptions() options.add_argument('-headless') driver = webdriver.Firefox(options=options) try: driver.set_window_size(1280, 800) driver.get(url) driver.get_full_page_screenshot_as_file('full-page.png') finally: driver.quit()
With Chrome, driver.save_screenshot() returns only the viewport. You have three ways to get the whole page from Chrome: call the DevTools protocol, resize the window, or scroll and stitch viewport screenshots. We ran all of them on the same pages and measured height limits, time and correctness. The short version: use Firefox's native call when you can, CDP clips when you need Chrome, and avoid stitching. For the basics of Selenium screenshots, read Selenium screenshots; for other tools, see full page screenshots compared.
Tested on 2026-10-15 with Python 3.14.8, Selenium 4.51.0, Pillow 12.3.0, Google Chrome 155.0.8059.40 (headless new) and Firefox 158.0 with geckodriver 0.37.1 (headless), on macOS 27.0.1. Times are medians of 3 runs against a local server.
Setup
pip install selenium==4.51.0 pillow==12.3.0
Selenium Manager downloads chromedriver and geckodriver for you when it can reach the internet. If your CI blocks that, install the drivers yourself and pass service=webdriver.FirefoxService(executable_path=...), which is what we did for Firefox.
The four methods on a 20,000 px page
Our test page is 20,000 px of 1,000 px colored bands with a magenta END band. A script checks every band in the output.
| Method | Output | Correct | Median time |
|---|---|---|---|
Firefox get_full_page_screenshot_as_png() |
1280 x 20,000 | Yes | 160 ms |
Chrome CDP Page.captureScreenshot, one call |
1280 x 20,000 | No: repeats the top after 16,384 px | 273 ms |
| Chrome CDP, 8,000 px clips pasted with Pillow | 1280 x 20,000 | Yes | not timed |
| Chrome window resized to the page height | 1280 x 19,857 | Bottom 143 px missing | 20,978 ms |
| Chrome scroll and stitch | 1280 x 20,000 | Yes on this page | 1,846 ms |
| Firefox scroll and stitch | 1280 x 20,000 | Yes on this page | 655 ms |
Firefox's native call was the fastest and the simplest. The single CDP call in Chrome produced a file of the right size whose last 3,616 px showed the top of the page again. Resizing the window was the slowest by two orders of magnitude, and in headless Chrome the window size includes 143 px of browser UI, so the image ended before the page did.
Height limits
| Method | Largest correct capture | Above it |
|---|---|---|
Firefox get_full_page_screenshot_as_png |
65,000 px (tested) | 100000 exceeds the maximum allowed screenshot height of 65535 pixels |
| Chrome CDP, one call | 16,384 device px | Image has the right size, content repeats from the top |
| Chrome CDP, clips | 40,000 px tested | — |
One detail from our runs: in one session, the single CDP call rendered 32,000 and 50,000 px pages correctly, after the window had earlier been resized to 20,000 px tall. In fresh sessions it repeated content every time: 7 captures at heights from 20,000 to 50,000 px. Do not depend on the state of the session; use clips.
Firefox: get_full_page_screenshot_as_*
Selenium's Python bindings have three variants on the Firefox driver:
driver.get_full_page_screenshot_as_file('page.png') # writes a file, returns True or False png = driver.get_full_page_screenshot_as_png() # bytes b64 = driver.get_full_page_screenshot_as_base64() # str
They call a geckodriver extension command, so they exist only on webdriver.Firefox, not on Chrome, Edge or Safari. The width is the window's content width; set the window size before get() so the layout matches what you want.
Chrome: Page.captureScreenshot in clips
execute_cdp_cmd sends a Chrome DevTools Protocol command through chromedriver. captureBeyondViewport: true with a clip lets Chrome draw a region outside the window. To stay under the 16,384 px limit, capture 8,000 px at a time and paste the parts with Pillow:
import base64 import io import sys from PIL import Image from selenium import webdriver SEGMENT = 8000 # CSS px per capture; stays below Chrome's 16,384 px texture limit def full_page_png(driver, path): width = driver.execute_script('return document.documentElement.scrollWidth') height = driver.execute_script('return document.documentElement.scrollHeight') canvas = Image.new('RGB', (width, height), 'white') for top in range(0, height, SEGMENT): clip = {'x': 0, 'y': top, 'width': width, 'height': min(SEGMENT, height - top), 'scale': 1} shot = driver.execute_cdp_cmd('Page.captureScreenshot', { 'format': 'png', 'captureBeyondViewport': True, 'clip': clip, }) canvas.paste(Image.open(io.BytesIO(base64.b64decode(shot['data']))), (0, top)) canvas.save(path) return width, height url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com' options = webdriver.ChromeOptions() options.add_argument('--headless=new') options.add_argument('--window-size=1280,800') driver = webdriver.Chrome(options=options) try: driver.get(url) print(full_page_png(driver, 'full-page.png')) finally: driver.quit()
On our 40,000 px page this produced all 40 bands and the END band. The code assumes a device pixel ratio of 1, which is what headless Chrome uses by default. If you emulate a higher ratio, multiply the paste position and canvas size by it, and use SEGMENT = 8000 // ratio.
execute_cdp_cmd exists on Chromium-based drivers (Chrome and Edge). It is not part of the WebDriver standard; Selenium is moving toward WebDriver BiDi, whose browsingContext.captureScreenshot command takes an origin of "document" for full-page captures. We used CDP because it is what Selenium 4.51 exposes directly in Python.
Why scroll and stitch is the last resort
Stitching takes a viewport screenshot, scrolls one screen, takes another, and pastes them together. It works in every browser, which is why many older answers recommend it. On a plain page it was correct. On our page with a fixed header and a fixed cookie bar, it produced a copy of each on every screen:
| Method on the sticky page | Header copies | Cookie bar copies |
|---|---|---|
Firefox get_full_page_screenshot_as_png |
1 | 1, across the content where the first screen ended |
Chrome CDP Page.captureScreenshot |
1 | 1, same place |
| Chrome window resized to the page height | 1 | 1, at the bottom |
| Chrome scroll and stitch | 7 | 7 |
| Firefox scroll and stitch | 7 | 6 |
Chrome needed 7 screens, not 6, because headless Chrome's 800 px window has a 657 px viewport. Resizing has its own problem: sections sized with 100vh grow with the window. In our Puppeteer test of the same page, a resized viewport made the hero 4,604 px tall.
Stitching also triggers scroll-dependent effects (parallax, headers that shrink on scroll, animations on reveal) that then differ from screen to screen. If you must stitch, hide fixed elements first; the sticky header guide has the JavaScript, which you can run with driver.execute_script.
Lazy images in Selenium
No full-page method scrolls, so lazy images below the first screen stay as placeholders. Run the scroll and wait in the page before the capture. In Selenium, execute_async_script passes a callback as the last argument:
driver.execute_async_script(''' const done = arguments[arguments.length - 1]; (async () => { for (let y = 0; y < document.documentElement.scrollHeight; y += innerHeight) { scrollTo(0, y); await new Promise((r) => setTimeout(r, 150)); } scrollTo(0, 0); await Promise.all([...document.images] .filter((img) => img.getAttribute('src') && !img.complete) .map((img) => img.decode().catch(() => {}))); })().then(done, done); ''')
Raise the script timeout first if pages are long: driver.set_script_timeout(60). On our lazy page (20 images, 800 ms each) this took the native Firefox capture and the CDP Chrome capture from 0 loaded images to all 20. The lazy image guide measures why both the scroll and the wait are needed.
Java and other bindings
The same three approaches exist in every binding. In Java, FirefoxDriver has getFullPageScreenshotAs(OutputType.FILE), and ChromeDriver has executeCdpCommand("Page.captureScreenshot", params). The 16,384 px Chrome limit and the 65,535 px Firefox limit come from the browsers, so they apply in every language.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
AttributeError: ... has no attribute 'get_full_page_screenshot_as_file' |
You are using Chrome or Edge | Use Firefox, or CDP clips |
| Chrome capture repeats the top of the page | Single CDP call above 16,384 device px | Clips of 8,000 px |
exceeds the maximum allowed screenshot height of 65535 pixels |
Firefox limit | Capture in parts with window.scrollTo plus get_screenshot_as_png, or reduce the page |
| Image 143 px shorter than the page | Resized window includes browser UI | Use CDP clips instead of resizing |
| Resize method takes 20 s | Chrome lays out and paints a huge window | Use CDP clips |
| Header and cookie bar on every screen | Scroll and stitch | Use a native method, or hide fixed elements first |
| Lazy images missing | No scroll before the capture | execute_async_script scroll and wait |
Unable to obtain driver for firefox |
Selenium Manager could not download a driver or find Firefox | Install geckodriver and pass FirefoxService(executable_path=...) |
ScriptTimeoutException during the scroll |
Default script timeout too short | driver.set_script_timeout(60) |
The same capture with one API call
If Selenium is only in your stack to take screenshots, an API removes the driver management. RenderScreenshot's full_page preset scrolls for lazy content before it captures:
import os from renderscreenshot import Client, TakeOptions client = Client(os.environ['RS_API_KEY']) image = client.take(TakeOptions.url('https://example.com').preset('full_page').format('png')) with open('full-page.png', 'wb') as f: f.write(image)
The scroll step, delay and the max_height cap (16,000 px by default) are in the full-page API guide. Sign up for a free key to compare the output with your Selenium capture.
Related in this topic
- Full Page Screenshots: Every Method Tested on One Page How to take a full page screenshot with DevTools, Puppeteer, Playwright, Selenium or an API. Each method tested on one 20,000 px page, with times and failures.
- Full-Page Screenshots: Scroll, Lazy Images and Height Capture a whole page with the full page screenshot API: full_page mode, scroll simulation for lazy images, step, delay and max_height, with SDK examples.
- Full Page Screenshot in Chrome Without an Extension Take a full page screenshot in Chrome with the DevTools command, the command line or a Console snippet. Tested limits, lazy images and fixed headers.
- Playwright Full Page Screenshot: Limits and Lazy Images Playwright full page screenshot guide: fullPage limits measured per engine, a wheel-scroll helper for lazy images and the style option for sticky headers.
- Puppeteer Full Page Screenshot: Limits, Lazy Images, Fixes Puppeteer full page screenshot guide: the 16,384 px limit we measured, captureBeyondViewport, lazy-load scrolling, sticky headers and a tested script.
- Screenshot Lazy-Loaded Images: 5 Strategies Measured Why full page screenshots miss lazy-loaded images, and which scroll strategy fixes it. Five strategies measured in Chromium, Firefox and WebKit, with code.