Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

In a standalone Pyppeteer script, put the browser work inside one asyncio.run(main()) call and await browser cleanup before main() returns. If the traceback points to Pyppeteer’s launcher cleanup running at interpreter exit, it may be trying to use a loop that has already closed. In a notebook, framework, or test runner, first check who owns the loop: the standalone pattern may not apply unchanged.

What “Event loop is closed” means

Python’s asyncio event loop runs asynchronous tasks, callbacks, and I/O. Once closed, it cannot be reused; Python’s documentation says that no other loop methods should be called after closure. The exception means something attempted to use a loop after that point, not that Pyppeteer is necessarily the only source of the problem. See Python 3.12’s event-loop documentation.

One Pyppeteer-related report illustrates a particular shutdown-order failure: its traceback shows the launcher’s _close_process atexit callback calling run_until_complete(self.killChrome()) after the loop has closed. It also reports that killChrome was never awaited. This makes late launcher cleanup worth investigating when those names appear in your own traceback, but a single report does not establish a universal cause or affected version. Read the Pyppeteer-related report alongside your own stack trace.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Identify which loop and cleanup path failed

Start with the first traceback frame that belongs to your code or Pyppeteer, rather than assuming that every instance has the same fix. The key questions are who owns the loop, when browser shutdown runs, and whether that shutdown is awaited while the loop is still active.

  • Launcher cleanup at exit: If the frames mention _close_process, killChrome(), or an atexit callback, check whether browser cleanup was deferred until after your main async function returned.
  • Your application’s loop management: Look for manual loop creation or closure, multiple top-level runners, or retained browser objects used after their owning coroutine has finished.
  • Host-managed execution: A web framework, notebook, or test runner may already own or scope the loop. A second top-level runner or closing the host’s loop can create a separate lifecycle conflict.
  • Other shutdown callbacks: A callback or subprocess transport can also surface a closed-loop error during shutdown. A historical Python tracker report describes a subprocess cleanup issue in asyncio; it is related context, not evidence that Pyppeteer caused that report. See Python issue 43884.

Record the complete traceback and note whether the error occurs during navigation, during explicit browser shutdown, at interpreter exit, or only in a particular host environment. Those details distinguish a browser-cleanup timing problem from another use-after-close path.

Fix a standalone script with one loop owner

For an ordinary script that owns its own entry point, use the high-level asyncio.run() API once. Put asynchronous Pyppeteer work inside that coroutine and close the browser before it returns. Python recommends high-level asyncio functions such as asyncio.run() for application developers; that runner manages its loop lifecycle, so do not separately close or shut down the loop it manages.

  1. Move the Pyppeteer operations into an async def main() function.
  2. Launch the browser and perform page work inside main().
  3. Await page cleanup if you explicitly created a page, then await browser cleanup in a finally block so it runs if page work raises an exception.
  4. Call asyncio.run(main()) once at the script’s entry point. Do not try to reuse that loop after the call returns.

This example shows the intended ordering. It is a general lifecycle pattern, not a claim that the same code has been tested against every Python and Pyppeteer release. Confirm the browser and page cleanup methods against the version installed in your project.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import asyncio
from pyppeteer import launch

async def main():
    browser = await launch()
    try:
        page = await browser.newPage()
        try:
            await page.goto("https://example.com")
            # Read or save page data here.
        finally:
            await page.close()
    finally:
        await browser.close()

if __name__ == "__main__":
    asyncio.run(main())

The nested finally blocks express the cleanup order: attempt page cleanup while the coroutine is still running, then attempt browser cleanup before it exits. If navigation or page handling raises an exception, the browser cleanup is still reached. Do not add a later exit handler that calls run_until_complete() on the loop after asyncio.run() has finished.

Adapt the fix to notebooks, frameworks, and test runners

The standalone script pattern assumes your application owns the top-level loop. That assumption may be false in an interactive notebook, an asynchronous web application, or a test runner. In those cases, use the host’s supported mechanism for running async code and keep browser cleanup within the lifecycle the host provides. Do not start a second top-level runner or close a loop owned by the host merely to make the exception disappear.

The correct integration syntax depends on the specific framework, notebook, test runner, and installed versions. The cited Pyppeteer report does not establish a framework-specific fix, so check the guidance for your actual environment rather than applying a generic workaround. In a test, for example, inspect when the fixture or test runner creates and tears down its loop, then ensure the browser is closed within that managed scope.

Common errors and what to change

Symptom Likely issue to investigate Action
Traceback names _close_process or an atexit callback Launcher cleanup may be trying to run after loop shutdown, as in the reported Pyppeteer case. Move awaited browser cleanup into the active coroutine and inspect whether another exit handler is trying to close the browser later.
Your code calls run_until_complete() on a closed loop The code is attempting to reuse a loop after closure. Do not revive it. Give the standalone script one asyncio.run(main()) lifecycle, or follow the host’s async integration if it owns the loop.
The error occurs only in a notebook or framework The host may already manage the running loop. Remove assumptions copied from a standalone script and use the environment’s documented async entry point and cleanup scope.
The error appears during subprocess or transport shutdown A callback may be running after loop shutdown; this is not necessarily a Pyppeteer launcher failure. Inspect the full traceback and cleanup order. Compare the relevant asyncio path with the context of Python issue 43884, without assuming it has the same cause.
Suppressing the exception appears to make the script finish The exception may be hiding incomplete browser-process cleanup. Fix cleanup ownership and timing instead of discarding the error; confirm that browser shutdown is awaited before the loop ends.

What to include if it still fails

If cleanup is already awaited inside the active coroutine and the exception persists, collect enough detail to identify which component is using the loop late. Include:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • The complete traceback, including the first frame in your own code and any launcher, callback, or subprocess frames.
  • Your Python and Pyppeteer versions, the operating system, and the command or runner used to start the program.
  • Whether the code runs as a standalone script, web request, notebook cell, or test fixture.
  • When the error occurs: during browser launch, page work, explicit cleanup, or interpreter/test-runner shutdown.

For version details, python --version reports the interpreter version, and pip show pyppeteer reports the installed package metadata in an environment where that pip corresponds to the project’s Python. Do not assume an issue-specific version workaround: the cited report does not identify one universal affected Python or Pyppeteer release.

Or skip the browser setup

If your goal is to capture a website rather than to control a browser for other automation, ScreenshotNeo offers a screenshot API and MCP server. A GET request returns a PNG, JPEG, WebP, or PDF; its options include full-page capture, CSS selector capture, viewport and device settings, and wait conditions. The API uses an access key. See the ScreenshotNeo API documentation.

Here is a one-request Python example:

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)

Replace the example URL with the page you want to capture and provide your API key. The same endpoint can be called with cURL or Node.js:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie or consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each of those steps can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses include X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. The same features are available on every plan.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Sign up for 1,000 free screenshots a month, with no card required.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Verdict

Use the traceback to determine who closed the loop and who is trying to use it afterward. For a standalone script, keep Pyppeteer work and awaited browser cleanup within one asyncio.run(main()) lifecycle. For a host-managed loop, follow the host’s async integration instead of imposing the standalone pattern.

Frequently Asked Questions

Does this exception prove that Pyppeteer itself is broken?

No. The exception identifies a closed-loop access, while the traceback and execution context are needed to identify which component attempted it.

Does the cited Pyppeteer report establish which release introduced the problem?

No. It is an individual report and does not establish a universal affected release or compatibility boundary.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I safely ignore a shutdown-time traceback if the screenshot was saved?

That depends on what the traceback shows. A shutdown exception may indicate incomplete browser-process cleanup, so inspect the cleanup frames before deciding it is harmless.

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.