Skip to content

[Bug]: Object leak in Tracing #2977

Description

@TomasBernotenas

Version

1.55.0

Steps to reproduce

Example steps (replace with your own):

  1. Clone my repo at (https://github.com/TomasBernotenas/playwright_tracing_leak)
  2. pip install -r requirements.txt
  3. python memleak_test.py
  4. you should see memory and object count increasing after each loop

if tracing is turned off then the memory stays stable and there are no additional objects created after each loop

Expected behavior

Object count and memory should return to initial after tracing is stopped

Actual behavior

When tracing is stopped callback objects keep accumulating

Additional context

No response

Environment

- Operating System: [Windows 11 10.0.22631]
- CPU: [(8) x64 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz]
- Browser: [All, Chromium, Firefox, WebKit]
- Python Version: [3.11.9]

Activity

  1. aarenchyma commented on Sep 19, 2025

    @aarenchyma
    • I ran a test with and without tracing across 30 iterations.

    • Both scenarios show a small memory increase in the first few iterations (stabilizing after ~39 MB).

    • No continuous growth is observed after stabilization.

    • This suggests normal caching behavior rather than a memory leak.

    Here’s a comparison chart for reference:

    Image

    You can reproduce with this script:

    import psutil
    import os
    from playwright.sync_api import sync_playwright
    def memory_usage():
        process = psutil.Process(os.getpid())
        return process.memory_info().rss / (1024 * 1024)  # MB
    def run_test(use_tracing=False, iterations=30):
        with sync_playwright() as p:
            for i in range(1, iterations + 1):
                browser = p.chromium.launch()
                context = browser.new_context()
                if use_tracing:
                    context.tracing.start(screenshots=True, snapshots=True, sources=True)
    
                page = context.new_page()
                page.goto("https://example.com")
    
                if use_tracing:
                    context.tracing.stop(path=f"trace_{i}.zip")
    
                browser.close()
    
                print(f"[after iteration {i}] Memory usage: {memory_usage():.2f} MB")
    
    print("=== Running without tracing ===")
    run_test(use_tracing=False)
    
    print("\n=== Running with tracing ===")
    run_test(use_tracing=True)
  2. TomasBernotenas commented on Sep 20, 2025

    @TomasBernotenas
    Author

    Tested your code and seems that leaking behavior is only reproducible with async_playwright, so i modified your code to async and then the leak can be observed.

    Things I changed:

    • Added gc object printing so that the growth would be seen
    • Changed to async_playwright
    • Added more goto actions, seems to speed up the object creation

    Results after just 30 loops

    Image

    Modified code:

    import asyncio
    import psutil
    import os
    import gc
    from collections import Counter
    from playwright.async_api import async_playwright
    
    def memory_usage():
        process = psutil.Process(os.getpid())
        objects = gc.get_objects()
    
        type_counter = Counter(type(obj) for obj in objects)
    
        print("Top 15 object types by count:")
        for obj_type, count in type_counter.most_common(15):
            print(f"{count:>8} objects | {obj_type}")
        return process.memory_info().rss / (1024 * 1024)  # MB
    
    async def run_test(use_tracing=False, iterations=30):
        async with async_playwright() as p:
            for i in range(1, iterations + 1):
                browser = await p.chromium.launch()
                context = await browser.new_context()
                if use_tracing:
                    await context.tracing.start(screenshots=True, snapshots=True, sources=True)
    
                page = await context.new_page()
                for _ in range(100):
                    await page.goto("https://playwright.dev/")
    
                if use_tracing:
                    await context.tracing.stop(path=f"trace_{i}.zip")
    
                await browser.close()
                print(f"[after iteration {i}] Memory usage: {memory_usage():.2f} MB")
    
    if __name__ == "__main__":
        asyncio.run(run_test(True))
    
  3. aarenchyma commented on Sep 21, 2025

    @aarenchyma

    Hey, thanks a lot for taking the time to dig into this 🙏. I see you modified the script to use async_playwright and could reproduce the leak there. In my case, I was originally running with sync_playwright, and I didn’t hit the leak.

    Since you’ve now confirmed the async version behaves differently, would you prefer that we continue debugging on the async side, or stick with the sync version (which doesn’t show the issue on my end)? Just want to be sure we’re focusing on whichever one you want to use going forward.

  4. TomasBernotenas commented on Sep 21, 2025

    @TomasBernotenas
    Author

    I think this issue will be related to how callbacks are handled because after further investigation I noticed that playwright._impl.ProtocolCallback object keep growing which hold the traceback objects so it will probably be issue of not cleaning up properly. Additionally noticed that those object seem to be left uncollected everywhere where send_no_reply function is used. Will try to investigate further

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions