How to Use Firefox in Headless Browser Automation
Run Firefox without a GUI using geckodriver and WebDriver, with setup, profiles, containers, troubleshooting, and a ScreenshotNeo shortcut.
Short answer: run Firefox with the --headless option, then control it through a W3C WebDriver client and geckodriver. Selenium is the most common client. Install compatible Firefox, geckodriver and Selenium versions, put geckodriver on PATH (or configure its path), enable headless mode in Firefox options, and verify a small navigation before adding your real test flow. Headless removes the graphical window; geckodriver is the HTTP WebDriver server that starts Firefox and translates WebDriver commands.
This guide shows a practical Python/Selenium setup, equivalent command-line checks, profile and container-package fixes, and ways to capture screenshots reliably. It is based on Mozilla’s Firefox command-line reference and geckodriver documentation.
1. Understand the headless stack
There are three separate pieces:
- Firefox: the browser. Its
--headlessflag runs without a GUI on Windows, Linux (GTK), and macOS. - geckodriver: a local HTTP server implementing WebDriver for Gecko browsers and forwarding commands to Firefox’s remote protocol.
- Client: Selenium or another W3C WebDriver-compatible library in your language.
Headless mode only suppresses the display. It does not replace WebDriver, and it does not make a page load faster or bypass bot protection.
2. Check versions before writing tests
- Record the Firefox version on the machine or container.
- Record the geckodriver version (
geckodriver --version). - Record the client version (for example,
python -m pip show selenium). - Compare the combination with Mozilla’s current support table before pinning it in CI.
The researched table lists geckodriver 0.37.1 and 0.37.0 with Firefox 115 ESR or later and Selenium 3.11 or later (the table shows Python 3.14 or later for those entries). Treat those values as a point-in-time reference, not a permanent compatibility guarantee. Mozilla also cautions that geckodriver is not fully WebDriver-conformant or completely Selenium-compatible, so check the table again when upgrading.
3. Make geckodriver discoverable
The simplest setup puts the geckodriver executable on PATH. A Selenium client can then find it automatically. For a hermetic build, keep the binary in a known directory and pass its explicit path through the client or your framework’s driver service configuration. Do not assume the Firefox binary and geckodriver are in the same directory.
# Verify the two executables are visible
firefox --version
geckodriver --version
# Verify the driver can start its local WebDriver server
geckodriver --port 4444
The last command is a diagnostic process; stop it after confirming that it binds locally. By default geckodriver listens on 127.0.0.1 and applies host/origin restrictions. A Selenium client normally starts and stops its own driver process, so you usually do not run this command in the test itself.
4. Minimal Python Selenium example
Install Selenium in the same environment as your test code:
python -m pip install selenium
The following script configures headless Firefox, opens a page, prints its title, saves a viewport screenshot, and always closes the session. It assumes Firefox and geckodriver are already installed and geckodriver is on PATH.
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
options = Options()
options.add_argument("--headless")
# options.binary_location = "/path/to/firefox" # set this when Firefox is not on PATH
with webdriver.Firefox(options=options) as driver:
driver.set_window_size(1366, 900)
driver.get("https://example.com")
print(driver.title)
driver.save_screenshot("example.png")
You can also set the headless environment documented by Mozilla for testing with MOZ_HEADLESS=1; MOZ_HEADLESS_WIDTH and MOZ_HEADLESS_HEIGHT define virtual display dimensions in that context. Keeping the option in code makes the test’s intent visible.
5. Navigation, waits, and screenshots that do not race the page
A screenshot taken immediately after get() can capture a loading shell. Use explicit waits for a meaningful element, a bounded delay for known animations, or a framework condition tied to your application. Keep the timeout finite so a broken page cannot stall the whole suite.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
with webdriver.Firefox(options=options) as driver:
driver.set_window_size(1440, 1000)
driver.get("https://example.com")
WebDriverWait(driver, 20).until(
EC.presence_of_element_located((By.TAG_NAME, "body"))
)
WebDriverWait(driver, 20).until(
EC.title_contains("Example")
)
driver.save_screenshot("ready.png")
For a full-page image, browser support and page layout matter. A portable approach is to scroll in controlled increments and stitch viewport images in your own code, or use a WebDriver/client feature available in your binding. Do not confuse Firefox’s command-line --screenshot [path] and --window-size width,height options with a scripted WebDriver session: those flags are useful for a simple one-off capture, while WebDriver is needed for interaction and waits.
6. Firefox options you will commonly need
| Need | Approach | Why it matters |
|---|---|---|
| Headless execution | options.add_argument("--headless") |
Runs without a visible GUI. |
| Nonstandard Firefox location | Set the client’s explicit binary location. | Useful with custom installations and containers. |
| Stable viewport | driver.set_window_size(width, height) or Firefox’s --window-size for CLI screenshots. |
Responsive breakpoints change the rendered page. |
| Clean isolated state | Use the default temporary profile. | Prevents cookies and extensions from leaking between tests. |
| Prepared state | Pass a custom profile through Firefox arguments or an encoded profile capability. | Use only when preferences, certificates, or authenticated state must be preloaded. |
| Diagnostics | Start geckodriver with -v (debug) or -vv (trace). |
Shows session creation, profile, and protocol failures. |
Mozilla documents a Marionette-port caveat when using the --profile route for custom profiles; explicitly setting the port is the documented workaround. The normal geckodriver behavior is to create a throwaway profile and remove it when the session ends, although interrupted sessions can leave temporary profiles behind.
7. Profiles, cookies, and repeatable tests
Prefer a fresh profile per test or per job. A shared profile introduces order dependence through cookies, local storage, permissions, extensions, and cache. If you need authenticated state, create a dedicated copy for the job and keep its path writable by both Firefox and geckodriver. Never put production credentials in a profile committed to source control.
When a session is interrupted, clean up orphaned temporary profiles according to your runner’s lifecycle rules. If a custom profile causes a startup hang, remove it and first prove that a default temporary profile works; then add preferences one at a time.
8. Firefox packaged in Snap, Flatpak, or another container
On Ubuntu 22.04 and later, Mozilla documents a failure mode where container-packaged Firefox sees a different filesystem from geckodriver. geckodriver creates a profile that Firefox cannot access, and startup can hang. Fix it by running Firefox and geckodriver in matching environments or by setting geckodriver’s --profile-root to a directory readable and writable by both processes. Also verify the explicit Firefox binary path and the geckodriver path.
# Diagnostic shape; choose a shared directory appropriate to your image
mkdir -p /tmp/firefox-profiles
geckodriver --profile-root /tmp/firefox-profiles -vv
The shared directory must satisfy the permissions and confinement rules of the package. If it does not, use a conventional Firefox installation or a single container environment that contains both processes.
9. Command-line headless screenshots without WebDriver
For a one-off image with no scripted interaction, Firefox exposes a command-line screenshot mode:
firefox --headless --window-size 1366,900 --screenshot page.png https://example.com
Use WebDriver when you need clicks, JavaScript, waits, cookies, multiple pages, assertions, or repeatable test setup. The command-line path is intentionally smaller and has fewer controls.
10. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
geckodriver executable needs to be in PATH |
Client cannot locate the driver. | Put geckodriver on PATH or configure its absolute path in the driver service. |
| Session fails immediately with a compatibility error | Firefox, geckodriver, and client versions do not match. | Check Mozilla’s support table, then pin a compatible trio. |
| Firefox binary not found | Custom or packaged installation. | Set the explicit Firefox binary location and verify it is executable. |
| Headless session hangs while starting | Snap/Flatpak filesystem boundary or an inaccessible profile. | Use matching environments or a shared --profile-root; run geckodriver with -vv. |
| Blank or incomplete screenshot | Capture happened before content or fonts loaded. | Wait for a specific selector, title, or application-ready condition; use a bounded timeout. |
| Elements are in the wrong layout | Viewport differs between headed and headless runs. | Set a deterministic window size and, when needed, headless display dimensions. |
| State leaks between tests | Reused profile, cookies, or cache. | Use a fresh temporary profile or an isolated copy per test job. |
| Custom profile will not start | Marionette port or permissions issue. | Try the default profile, then follow Mozilla’s explicit-port workaround and verify ownership. |
11. Reliability, speed, and cost
- Reliability: pin browser and driver versions in CI, log versions at job start, use explicit waits, and always quit the driver in a
finallyblock or context manager. - Speed: reuse a session for related pages when isolation allows it; avoid arbitrary long sleeps; wait on the smallest useful readiness condition.
- Parallelism: give each worker its own profile root, temporary directory, and output path. Do not make workers share a profile.
- Resource limits: headless still consumes CPU, memory, disk, and network bandwidth. Set job-level timeouts and cap concurrent sessions to what the host can sustain.
- Cost: Selenium, Firefox, and geckodriver are software you run and operate. Your direct costs are the machines, CI minutes, storage, and network traffic; the research sources provide no universal benchmark for these.
12. Or skip the browser setup
If your goal is a clean website image or PDF rather than browser test control, ScreenshotNeo provides a single HTTP request. Its capture flow accepts cookie/consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. It also provides an MCP server for AI agents with take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options. This is the one-call image example:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every plan includes the features: full-page and element capture, device presets or custom viewports, retina scale, dark mode, PDF controls, custom CSS/JavaScript, clicks, waits, blocking rules, headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen-TTL caching, signed links, async webhooks, bulk capture (100 URLs per call), usage API, and OpenAPI support. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
13. FAQ
Does headless Firefox need Xvfb?
No. Firefox’s native --headless mode is designed to run without a GUI. Adding a virtual display is a separate architecture choice.
Can I automate Firefox without Selenium?
Yes. geckodriver exposes the WebDriver HTTP API, so any conforming W3C WebDriver client can use it.
Should I keep one Firefox process for the whole suite?
Reuse can reduce startup overhead, but isolate tests that depend on clean cookies, storage, permissions, or profiles. Choose based on state isolation requirements.
Why does a page look different in headless mode?
Viewport dimensions, fonts, device pixel ratio, media queries, animations, and timing can differ. Set the viewport explicitly and wait for the page condition that matters.
When is ScreenshotNeo a better fit?
Use it when you need rendered screenshots or PDFs without maintaining Firefox, geckodriver, profiles, waits, and cleanup, especially when consent UI and failed captures would otherwise waste time.


