Recommended Free Tools
Headless Chrome downloads usually “suspend” for one of four reasons: Chrome cannot write to the configured directory, the Python process quits before the file is complete, the browser is running in a different machine or container, or Chrome and ChromeDriver are incompatible. Create a unique absolute download directory, grant the session download permission where required, wait for a completed file before calling quit(), and verify which filesystem actually contains the browser output.
Use a dedicated absolute directory and wait for completion
For a local Selenium session, configure downloads before creating the driver. Do not use the desktop or, on Linux, the home directory: ChromeDriver documents those and other system locations as disallowed or unreliable. Use a directory created specifically for the run, and pass its absolute path.
from pathlib import Path
from selenium import webdriver
out_dir = Path.cwd() / "downloads"
out_dir.mkdir(parents=True, exist_ok=True)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.add_experimental_option("prefs", {
"download.default_directory": str(out_dir.resolve()),
"download.prompt_for_download": False,
"download.directory_upgrade": True,
})
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com/report")
# Locate and click the site's actual download control here.
# driver.find_element(...).click()
finally:
pass # quit only after the completion check below
The directory and absolute-path preference are the essential settings. The prompt and directory-upgrade preferences are commonly used Chrome settings; validate them with the Chrome and Selenium versions installed in your environment rather than treating them as a guarantee that a site will download a file.
Wait for the file, not merely for the click
ChromeDriver does not wait for a download to finish. A click returning only means that the browser accepted the action. Poll for the expected filename and, where present, the disappearance of Chrome’s temporary partial-download file. Set a deadline so a failed transfer produces useful diagnostics instead of an infinite wait.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errors#1 Best Overall
import time
expected = out_dir / "report.csv"
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
partials = list(out_dir.glob("*.crdownload"))
if expected.exists() and not partials:
break
time.sleep(0.25)
else:
files = [p.name for p in out_dir.iterdir()]
raise TimeoutError(
f"Download did not complete: {expected}; directory contains {files}"
)
driver.quit()
The .crdownload suffix is an illustrative indicator, not a promise that every Chrome version or server response uses it. If the server chooses a random name, snapshot the directory before clicking, snapshot it again afterward, and identify the new completed file by name, size, or content.
A diagnostic sequence for a suspended download
- Record the environment. Log the Python version, Selenium version, Chrome version, ChromeDriver version, operating system, container image, and whether WebDriver is local or remote. Selenium’s Chrome guide requires matching Chrome and ChromeDriver major versions.
- Prove the path is writable. Create the directory before the browser starts, resolve it to an absolute path, and test that the same operating-system account running Chrome can create and delete a small file there. Avoid desktop and Linux home-directory paths described in ChromeDriver guidance.
- Confirm the browser action. The control might open a new tab, return an error page, require authentication, or produce a different filename. Inspect the current URL, window handles, page text, and directory contents immediately after the click.
- Check session download permission. Selenium Python exposes an
enable_downloadsoption in current versions. If the installed session requires it, set it before creating the driver (shown below). - Wait before closing. Keep the driver alive until the expected output exists and is complete. Calling
driver.quit()immediately can terminate an in-progress transfer. - For remote execution, find the browser’s filesystem. A path inside a Grid node or container is not automatically a path on the Python client. Use the download-retrieval or shared-volume mechanism documented by your Grid provider.
- Reduce the case. Reproduce one URL, one click, one directory, and one timeout while collecting browser and driver logs. Logging and protocol details vary by Selenium release.
Enable downloads in Selenium sessions that require it
In Selenium’s Python Chrome options reference for Selenium 4.49.0, enable_downloads indicates whether the session can download files. Use it in addition to the destination preference when your driver or remote service requires the capability.
from pathlib import Path
from selenium import webdriver
out_dir = (Path.cwd() / "downloads").resolve()
out_dir.mkdir(parents=True, exist_ok=True)
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.enable_downloads = True
options.add_experimental_option("prefs", {
"download.default_directory": str(out_dir),
"download.prompt_for_download": False,
"download.directory_upgrade": True,
})
driver = webdriver.Chrome(options=options)
Do not assume that adding the property compensates for a remote provider that does not expose files to the client; it only addresses session permission.
Use BiDi when your Selenium setup supports it
Selenium’s BiDi browser API provides an explicit download-behavior call. Allow downloads and supply the destination folder; the API requires a destination when downloads are allowed. This requires an established BiDi connection and support in the installed browser and Selenium versions, so it is not a drop-in method for every ordinary WebDriver instance.
# Illustrative BiDi flow; consult the installed Selenium 4 API for connection setup.
await driver.bidi_connection()
await driver.browser.set_download_behavior(
allowed=True,
destination_folder=str(out_dir),
)
Use the exact async/context-management pattern documented by your Selenium release. Keep the same absolute-path and completion-wait logic after enabling the behavior.
Why older CDP snippets can stop working
Many older examples call Chrome DevTools Protocol commands such as Page.setDownloadBehavior or Browser.setDownloadBehavior. Selenium describes CDP support as temporary while BiDi is implemented and warns that CDP is not designed as a stable testing API. Command names and parameters depend on the browser protocol version. If you must use CDP for a version-specific requirement, verify the command against the installed Chrome protocol and expect maintenance when Chrome changes.
Rank #3
Headless mode and version compatibility
Modern Headless Chrome uses the same browser implementation as regular Chrome. Chrome 112 changed Headless so Chrome creates platform windows without displaying them; from Chrome 132.0.6793.0, the old implementation is a separate chrome-headless-shell binary. For a normal current Selenium setup, use the regular Chrome binary with --headless=new rather than historical workarounds for the old implementation.
Selenium’s current Chrome guide lists Selenium 4 compatibility with Chrome 75 and later while requiring matching Chrome and ChromeDriver major versions. In continuous integration, pin compatible browser and driver versions and upgrade them together.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Local versus remote WebDriver: the path that matters
| Execution | Where the configured directory exists | What to verify |
|---|---|---|
| Local Chrome | The machine running Python and Chrome | The account running Chrome can write the absolute directory. |
| Remote Grid | The Grid node or browser container | The provider’s file-transfer API or shared volume moves the file to the client. |
| Containerized CI | The container filesystem | The directory is mounted or copied before the container exits. |
Seeing no file on the Python host does not prove Chrome failed. First inspect the node or container where the browser process runs. There is no universal retrieval method across Grid providers, so follow the documentation for the service you deployed.
Common symptoms and fixes
The directory remains empty
- Confirm the click actually triggered a download rather than navigation, a new tab, or an authentication error.
- Print the resolved directory and inspect it inside the browser environment.
- Use a newly created writable directory and avoid special system paths.
A partial file remains when the script ends
- Increase the timeout for large or slow responses.
- Do not call
quit()in afinallyblock until the completion check has passed; put cleanup after the wait, or retain the failure log before quitting. - Check whether the server redirects or streams a response whose final name differs from your expectation.
The session starts but downloads are blocked
- Set
options.enable_downloads = Truewhen supported or required. - For a supported BiDi session, call
set_download_behavior(allowed=True, destination_folder=...). - Ensure your remote service permits downloads; a browser capability cannot override provider policy.
The same code works locally but not in CI
- Compare Chrome and ChromeDriver major versions and the Selenium version.
- Check container permissions, writable mounts, and the actual browser-side path.
- Copy artifacts out of the container before teardown.
An old workaround fails after a Chrome upgrade
Replace obsolete headless assumptions and version-specific CDP commands with current Chrome and Selenium documentation. Prefer BiDi where your versions support it, and verify protocol commands when CDP is unavoidable.
Complete small example with deterministic cleanup
from pathlib import Path
import time
from selenium import webdriver
out_dir = (Path.cwd() / "downloads").resolve()
out_dir.mkdir(parents=True, exist_ok=True)
expected = out_dir / "report.csv"
options = webdriver.ChromeOptions()
options.add_argument("--headless=new")
options.enable_downloads = True
options.add_experimental_option("prefs", {
"download.default_directory": str(out_dir),
"download.prompt_for_download": False,
"download.directory_upgrade": True,
})
driver = webdriver.Chrome(options=options)
try:
driver.get("https://example.com/report")
# Replace with the site's real selector.
# driver.find_element(By.CSS_SELECTOR, "a.download").click()
deadline = time.monotonic() + 60
while time.monotonic() < deadline:
if expected.exists() and not list(out_dir.glob("*.crdownload")):
break
time.sleep(0.25)
else:
raise TimeoutError(f"Timed out waiting for {expected}")
finally:
driver.quit()
Adapt the filename and selector to the site. The example’s key ordering is deliberate: configure first, trigger the action, wait for observable completion, then close.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your actual goal is a clean image or PDF of a web page rather than downloading a site-generated file, ScreenshotNeo provides a one-request route. Its API accepts the URL and returns PNG, JPEG, WebP, or PDF; it can accept cookie banners before capture and remove more than 60 known consent platforms, newsletter popups, and chat widgets. Each response identifies whether the page was clean and billed through X-Page-Verdict and X-Billed headers; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed.
cURL
curl -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 request options. An MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
Best Value
Frequently Asked Questions
Should I use a fixed filename in the wait loop?
Only when the server supplies a predictable name. Otherwise compare directory contents before and after the click and select the new completed file.
Does headless mode itself prevent downloads?
Modern Headless uses the regular Chrome implementation; failures are more often path, permission, session, lifecycle, remote-filesystem, or version issues.
Can a shared download folder solve remote Grid transfers?
A shared volume can make the file reachable, but the Grid still needs correct mounting and permissions. Follow the provider’s transfer or volume documentation.
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.

