How to Stop the Asyncio Event Loop and Close Pyppeteer
Close Pyppeteer safely, stop or close asyncio in the right order, and avoid running-loop errors in scripts, notebooks, and services.
Short answer: await browser.close() inside your coroutine, then let it finish. For a standalone script use asyncio.run(main()); it performs async-generator and default-executor shutdown and closes its loop. If you created a loop yourself, stop it first, perform cleanup while it can run, and call loop.close() only when it is no longer running.
Browser shutdown and loop shutdown are separate. Pyppeteer’s Browser.close API closes connections and terminates the browser process.
1. Standalone script (recommended)
import asyncio
from pyppeteer import launch
async def main():
browser = await launch()
try:
page = await browser.newPage()
await page.goto('https://example.com', {'waitUntil': 'networkidle2'})
print(await page.title())
finally:
await browser.close()
if __name__ == '__main__':
asyncio.run(main())
asyncio.run owns this loop and closes it after main returns. Do not call loop.close() again.
2. A manually owned loop
Use this only when your application created the loop. Close Pyppeteer first, then stop and close the loop.
import asyncio
from pyppeteer import launch
async def capture():
browser = await launch()
try:
page = await browser.newPage()
await page.goto('https://example.com')
return await page.title()
finally:
await browser.close()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
print(loop.run_until_complete(capture()))
finally:
if loop.is_running():
loop.stop()
loop.run_until_complete(loop.shutdown_asyncgens())
# If you used the default executor, also run:
# loop.run_until_complete(loop.shutdown_default_executor())
loop.close()
For run_forever(), loop.stop() requests exit after the current callback batch; it does not close the loop. Python requires the loop to be stopped before loop.close(). Closing discards pending callbacks and cannot be undone. See the event-loop documentation.
3. Running loops: notebooks, servers and GUI apps
asyncio.run() cannot be called while another loop is running in the same thread. Use the host’s async entry point and leave its loop open:
async def cell():
browser = await launch()
try:
page = await browser.newPage()
await page.goto('https://example.com')
finally:
await browser.close()
# In an already-async notebook cell:
await cell()
4. Robust lifetime patterns
- Initialize
browser = Nonewhen launch and work are separate, then close conditionally infinally. - Pyppeteer documents
autoClose=Trueby default, but explicit awaited closure makes ownership clear. - Close a page when you only need to discard one tab; close the browser once all pages are finished.
- For services, cancel and await child tasks before loop shutdown, then run
shutdown_asyncgens()and, when used,shutdown_default_executor().
browser = None
try:
browser = await launch()
# browser work
finally:
if browser is not None:
await browser.close()
5. Troubleshooting
| Error or symptom | Cause | Fix |
|---|---|---|
asyncio.run() cannot be called from a running event loop |
Notebook or framework owns the loop. | Await your coroutine through that host. |
Cannot close a running event loop |
loop.close() ran before run_forever() returned. |
Call loop.stop(), let it return, then close in finally. |
| Browser process remains | Cleanup was skipped after an exception. | Put await browser.close() in finally. |
Event loop is closed |
Browser cleanup was attempted after loop closure. | Await browser closure before shutting down the loop. |
| Pending-task warnings | Tasks were still running. | Cancel and await them before loop shutdown. |
| Cleanup hangs | A page operation waits indefinitely. | Set operation timeouts, cancel the task, and retain the finally cleanup. |
6. Performance, reliability and cost
- Prefer
asyncio.run()for one-shot programs; it applies standard shutdown steps. - Reuse one loop and browser for batches instead of creating one per URL.
- Keep loop ownership at the application boundary; libraries should expose coroutines.
- Timeout navigation and tasks so a stuck page cannot block shutdown.
- Forced loop closure can discard callbacks and leave work incomplete. Browser shutdown itself has no separate fee; runtime cost is your browser and hosting work.
7. Or skip the browser setup
If you only need a screenshot, ScreenshotNeo returns PNG, JPEG, WebP or PDF from one GET request. It accepts consent banners and removes 60+ known consent platforms, newsletter popups and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and X-Page-Verdict/X-Billed report the result. Its MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
Options and configuration are in the ScreenshotNeo docs.
curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Free includes 1,000 shots/month with no card; paid plans start at $5 for 3,000, and every feature is on every plan. Create a free account.
8. FAQ
Does browser.close() stop asyncio?
No. It closes Pyppeteer’s browser and connection; the loop continues until its owner stops it.
Can I close the loop inside main()?
Not while main() runs on it. Return and let the outer owner close it.
Which entry point should I choose?
Use asyncio.run() for a standalone script; manual methods are for code that truly owns a loop.
What if launch fails?
There is no browser to close. Initialize to None and close conditionally.


