Recommended Free Tools
Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
A Pyppeteer “PermissionError” in a multiprocessing program is not one diagnosed problem. The same label can describe a Python or operating-system access denial, a browser request failure, or an error caused by process startup. Capture the complete traceback, identify the exact call that fails, and then apply the fix for that layer. Do not assume that a page permission setting will repair an operating-system exception.
Start with the traceback, not a guessed fix
Save the full exception, including the exception class, message, and stack frames. Record the Python version, operating system, Pyppeteer version, Chromium revision or executable path, multiprocessing start method, and the operation being attempted. The failing line normally places the problem in one of five layers:
- Process startup: creating a worker, importing the main module, or serializing its arguments.
- Browser launch: starting Chromium, creating a temporary directory, or opening a profile.
- Page or context creation: calling
browser.newPage()or creating a browser context. - Navigation or request handling: loading a URL, intercepting a request, or aborting a request.
- File and profile access: reading a local file, writing a download, or using a user-data directory.
Pyppeteer’s API reference documents browser contexts, pages, and request-abort error codes, including accessdenied, defined there as permission to access a non-network resource being denied. That is a browser request error code; it does not prove that a Python PermissionError came from the same cause. See the Pyppeteer API reference.
Free tools Windows power users keep installed
One-click scans. No signup required.
Make multiprocessing safe before changing browser permissions
Python’s spawn and forkserver methods import the main module in a new interpreter. The Python multiprocessing documentation requires process arguments to be picklable and says the main module must be safe to import without starting unintended child processes. Put process creation behind a main guard and pass simple data such as strings, numbers, and dictionaries.
#1 Best Overall
import asyncio
import multiprocessing as mp
from pathlib import Path
def worker(url: str, output: str) -> None:
# Create the browser in the worker that owns it.
asyncio.run(capture(url, output))
async def capture(url: str, output: str) -> None:
from pyppeteer import launch
browser = await launch({
"headless": True,
"args": ["--no-sandbox"], # Use only when your container policy permits it.
})
try:
page = await browser.newPage()
await page.goto(url, {"waitUntil": "networkidle2"})
await page.screenshot({"path": output, "fullPage": True})
finally:
await browser.close()
def main() -> None:
jobs = [
("https://example.com", "shots/example.png"),
("https://www.python.org", "shots/python.png"),
]
Path("shots").mkdir(exist_ok=True)
ctx = mp.get_context("spawn")
processes = [ctx.Process(target=worker, args=job) for job in jobs]
for process in processes:
process.start()
for process in processes:
process.join()
if process.exitcode != 0:
raise RuntimeError(f"worker exited with code {process.exitcode}")
if __name__ == "__main__":
mp.freeze_support()
main()
The --no-sandbox argument is an environment decision, not a universal permission fix. It may be required in a deliberately configured container running as a non-root user, but removing Chromium’s sandbox reduces isolation. Prefer correcting container users, filesystem ownership, and sandbox prerequisites when possible.
Do not pass live browser objects between processes
A Page, browser connection, event-loop object, or context is not established by the cited documentation as safely transferable between processes. Treat each worker as the owner of its own browser and pages: pass the URL and output configuration, launch inside the worker, close the browser there, and return serializable results. This avoids inherited sockets, event loops, and profile locks without claiming a Pyppeteer guarantee that the sources do not provide.
Rank #2
Diagnose filesystem and profile denials
If the traceback names open, mkdir, a download path, or a Chromium user-data directory, inspect the actual path as the same account that runs the worker. Confirm that the parent exists, is writable, and is not mounted read-only. Give each worker a distinct temporary profile and output filename; two Chromium processes writing one profile can produce locks and misleading failures.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →from pathlib import Path
import tempfile
profile = Path(tempfile.mkdtemp(prefix="pyppeteer-"))
output = Path("shots") / f"{worker_id}.png"
output.parent.mkdir(parents=True, exist_ok=True)
# Pass str(profile) as userDataDir and str(output) as the screenshot path.
On Linux, check directory ownership and mode with ls -ld; in containers, check the effective UID, volume ownership, and security policy. On Windows, verify that the service account has access to both the temporary directory and destination. Do not “fix” a denial by making an entire filesystem world-writable.
Separate browser request errors from Python PermissionError
Request interception can expose browser-level failures. If your handler receives an abort code such as accessdenied, log the request URL, resource type, and code before changing behavior. That code concerns a request denied by the browser; it is distinct from an exception raised while Python starts a process or opens a file.
async def log_request(request):
print(request.url, request.resourceType)
# Only abort for a policy you explicitly intend to enforce.
await request.continue_()
page.on("request", lambda request: asyncio.ensure_future(log_request(request)))
Remove interception temporarily when diagnosing. A handler that aborts every request, mishandles asynchronous callbacks, or blocks a required resource can look like a permission problem. Re-enable rules one at a time and record the exact abort code documented by your installed release.
Check launch, executable, and version details
Launch failures may be ordinary OS denials: an executable without execute permission, a blocked temporary directory, or a browser binary unavailable to the worker account. Print the resolved executable path, test it outside multiprocessing under the same account, and compare a single-process run with a one-worker run. If one process works but several fail, look for shared profile directories, port collisions, resource limits, and inherited state rather than granting broader permissions.
The Pyppeteer documentation is old, and the project’s issue tracker currently labels it unmaintained. Verify behavior against the package and browser versions actually installed. The project’s introductory documentation is at pyppeteer.github.io/pyppeteer.
Best Value
A controlled troubleshooting sequence
- Run the capture once in a fresh virtual environment, outside multiprocessing, with a new temporary profile.
- Use a local writable output directory and a public test URL. Remove request interception, custom headers, cookies, downloads, and JavaScript hooks.
- Run exactly one spawned worker. Keep the
if __name__ == "__main__":guard and pass only picklable arguments. - Give each worker a unique profile and output path. Close the browser in a
finallyblock. - Add features back individually: custom executable, proxy, request rules, local files, downloads, and multiple workers.
- When the failure returns, classify it from the traceback: startup/import, executable launch, filesystem, page API, navigation/request, or external security policy.
Common symptoms and targeted fixes
| Symptom | Likely layer | Checks and response |
|---|---|---|
| Child fails before your worker function runs | Process startup | Use the main guard, choose spawn explicitly while testing, and make every argument picklable. |
PermissionError names a profile or output path |
Filesystem | Check effective user, parent-directory write permission, read-only mounts, and per-worker paths. |
| Chromium executable cannot start | OS launch policy | Print the executable path, test it as the worker account, and inspect execute bits or Windows security controls. |
Navigation reports accessdenied |
Browser request | Log request details, disable interception, and review the rule or target policy; do not treat it as proof of Python access denial. |
| One worker succeeds; many fail | Shared state or limits | Separate profiles and files, cap concurrency, and inspect locks, file descriptors, memory, and container limits. |
When Playwright is being considered
Playwright is a separate automation library, not a Pyppeteer permission patch. Its Python BrowserContext documentation describes context-level permission grants, optionally scoped to an origin, and notes that supported permissions vary by browser and version. Use that API only when you have deliberately chosen Playwright and confirmed support for your browser; it does not change Python multiprocessing rules.
Or skip the browser setup
If your goal is simply a reliable website image or PDF, ScreenshotNeo provides a single HTTP call instead of managing Chromium workers. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
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)
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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 API documentation for options such as full-page capture, CSS selectors, device presets, custom CSS and JavaScript, waits, blocking rules, headers, cookies, geolocation, PDFs, caching, signed links, asynchronous jobs, bulk capture, and usage reporting. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
Frequently Asked Questions
Should I use fork or spawn with Pyppeteer?
Test with an explicit start method and follow Python’s documented safe-import and picklability rules. The traceback and environment determine the cause; neither method is a universal permission fix.
Can I reuse one Page object in several workers?
Do not assume that is safe. Create the browser and page inside the worker that uses them, and pass only serializable configuration between processes.
Does Playwright’s permission API fix Pyppeteer errors?
No. Playwright’s BrowserContext permission grants belong to Playwright and have browser/version limits; they are relevant only if you intentionally migrate.
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.
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

