Skip to content

CDP Mode leaks event loops, file descriptors and temp profile dirs in long-running processes #4408

Description

@montovaneli

Setup

  • SeleniumBase 4.50.1
  • Python 3.12.0
  • Windows 11/Ubuntu 24.04

The use case

I run a process that stays alive for a long time and starts/stops many CDP sessions in a loop (a worker that never restarts Python). After a while the process keeps growing in memory, I eventually hit "Too many open files" on Linux, and my temp folder fills up with uc_* profile directories.

For now I work around this by isolating each session in its own spawned child process, so when the child exits the OS reclaims everything (FDs, loops, sockets, Chrome, temp dirs). It works, but it never felt right to me, and it made me think there was probably room to improve cleanup in UC/CDP Mode itself, so I decided to dig in and track down where the leaks were actually coming from.

After looking into it, the main issue is that the real cleanup only runs at atexit, so if the process never exits nothing actually gets freed. Calling stop() (or quit()) ends the Chrome process but leaves the rest behind.

How to reproduce

import glob, os, tempfile, gc
from seleniumbase import sb_cdp

tmp = tempfile.gettempdir()
def uc_count():
    return len(glob.glob(os.path.join(tmp, "uc_*")))

for i in range(1, 11):
    sb = sb_cdp.Chrome("about:blank")
    sb.quit()
    gc.collect()
    print(f"after run {i}: uc_* temp dirs = {uc_count()}")

The uc_* count goes up by one every run and never comes back down (until the process exits). If you also watch open file descriptors (e.g. ls /proc/self/fd| wc -l on Linux), they keep climbing too. The same happens with Driver(uc=True).

What I found

A few separate things add up:

  1. Every Browser is added to the module-global __registered__instances__ set in start() and never removed. Because of that the browser (and its connection, websockets and subprocess pipes) stays referenced forever and can't be garbage-collected, even after the caller drops all references. (seleniumbase/undetected/cdp_driver/browser.py)

  2. Browser.stop() ends Chrome but, on the success path, the break skips the line that resets self._process, so the three subprocess pipes (stdin/stdout/stderr) stay open.

  3. The auto-generated temp profile dir (uc_*, created by temp_profile_dir() in config.py) is only deleted inside deconstruct_browser(), which only runs via atexit.register(...). stop() never removes it. Note this dir is created even in the connect-existing case, so it's an empty leftover folder every run.

  4. sb_cdp.Chrome creates two event loops: one inside cdp_util.start_sync and another in Chrome.__init__. Neither is closed, so each run leaks the loop's selector FD and self-pipe.

  5. The CDP event loop is only closed inside the with SB() context manager teardown (sb_manager.py). The Driver(uc=True) path and the pure-CDP path never close it.

So each cycle permanently pins roughly 5-8 file descriptors (selector FDs, self-pipes, the DevTools websocket, the subprocess pipes) plus a temp dir.

What I think could change

The fix that worked for me was to make stop() actually release things instead of leaving it all to atexit:

  • Remove the instance from __registered__instances__ in stop() so it can be collected.
  • Always clear self._process in stop().
  • Remove the auto-generated temp dir in stop() (with a small retry for Windows file locks).
  • Have sb_cdp.Chrome reuse a single event loop and remember it on the browser so stop() can close it.
  • Close the loop and websockets on the Driver(uc=True) and pure-CDP paths too, not just under with SB(). A small idempotent teardown helper called from quit()/stop() covers all three paths.

I tested this with real Chrome across pure CDP, Driver(uc=True), and with SB(uc=True), looping many start/stop cycles in one process: the uc_* count and the registry size stay flat, the loops get closed, and there's no leftover noise.

I know the project is mostly closed to PRs, so I'm filing this as an issue instead, but I already have a working patch I've validated locally (master...montovaneli:SeleniumBase:leak-fix). Feel free to analyze/use it if you want to.

Thanks a lot for your project!

Metadata

Metadata

Assignees

No one assigned

    Labels

    not enough infonot enough info / more info neededquestionSomeone is looking for answers

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions