Pyleak
Detect leaked asyncio tasks, threads, and event loop blocking with stack trace in Python. Inspired by goleak.
Install / Use
/learn @deepankarm/PyleakREADME
pyleak
<a href="https://pypi.org/project/pyleak/"><img alt="PyPI" src="https://img.shields.io/pypi/v/pyleak?label=Release&style=flat-square"></a> <a href="https://pepy.tech/projects/pyleak"><img src="https://static.pepy.tech/badge/pyleak" alt="PyPI Downloads"></a>
Detect leaked asyncio tasks, threads, and event loop blocking in Python. Inspired by Go's goleak.
Installation
pip install pyleak
Quick Start
import asyncio
from pyleak import no_task_leaks, no_thread_leaks, no_event_loop_blocking
# Detect leaked asyncio tasks
async def main():
async with no_task_leaks():
asyncio.create_task(asyncio.sleep(10)) # This will be detected
await asyncio.sleep(0.1)
# Detect leaked threads
def sync_main():
with no_thread_leaks():
threading.Thread(target=lambda: time.sleep(10)).start() # This will be detected
# Detect event loop blocking
async def async_main():
async with no_event_loop_blocking():
time.sleep(0.5) # This will be detected
Usage
Context Managers
All detectors can be used as context managers:
# AsyncIO tasks (async context)
async with no_task_leaks():
# Your async code here
pass
# Threads (sync context)
with no_thread_leaks():
# Your threaded code here
pass
# Event loop blocking (async context only)
async def main():
async with no_event_loop_blocking():
# Your potentially blocking code here
pass
Decorators
All detectors can also be used as decorators:
@no_task_leaks()
async def my_async_function():
# Any leaked tasks will be detected
pass
@no_thread_leaks()
def my_threaded_function():
# Any leaked threads will be detected
pass
@no_event_loop_blocking()
async def my_potentially_blocking_function():
# Any event loop blocking will be detected
pass
Get stack trace
From leaked asyncio tasks
When using no_task_leaks, you get detailed stack trace information showing exactly where leaked tasks are executing and where they were created.
import asyncio
from pyleak import TaskLeakError, no_task_leaks
async def leaky_function():
async def background_task():
print("background task started")
await asyncio.sleep(10)
print("creating a long running task")
asyncio.create_task(background_task())
async def main():
try:
async with no_task_leaks(action="raise"):
await leaky_function()
except TaskLeakError as e:
print(e)
if __name__ == "__main__":
asyncio.run(main())
Output:
creating a long running task
background task started
Detected 1 leaked asyncio tasks
Leaked Task: Task-2
ID: 4345977088
State: TaskState.RUNNING
Current Stack:
File "/tmp/example.py", line 9, in background_task
await asyncio.sleep(10)
Include creation stack trace
You can also include the creation stack trace by passing enable_creation_tracking=True to no_task_leaks.
async def main():
try:
async with no_task_leaks(action="raise", enable_creation_tracking=True):
await leaky_function()
except TaskLeakError as e:
print(e)
Output:
creating a long running task
background task started
Detected 1 leaked asyncio tasks
Leaked Task: Task-2
ID: 4392245504
State: TaskState.RUNNING
Current Stack:
File "/tmp/example.py", line 9, in background_task
await asyncio.sleep(10)
Creation Stack:
File "/tmp/example.py", line 24, in <module>
asyncio.run(main())
File "/opt/homebrew/.../asyncio/runners.py", line 194, in run
return runner.run(main)
File "/opt/homebrew/.../asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/opt/homebrew/.../asyncio/base_events.py", line 671, in run_until_complete
self.run_forever()
File "/opt/homebrew/.../asyncio/base_events.py", line 638, in run_forever
self._run_once()
File "/opt/homebrew/.../asyncio/base_events.py", line 1971, in _run_once
handle._run()
File "/opt/homebrew/.../asyncio/events.py", line 84, in _run
self._context.run(self._callback, *self._args)
File "/tmp/example.py", line 18, in main
await leaky_function()
File "/tmp/example.py", line 12, in leaky_function
asyncio.create_task(background_task())
TaskLeakError has a leaked_tasks attribute that contains a list of LeakedTask objects including the stack trace details.
Note:
enable_creation_trackingmonkey patchesasyncio.create_taskto include the creation stack trace. It is not recommended to be used in production to avoid unnecessary side effects.
From event loop blocks
When using no_event_loop_blocking, you get detailed stack trace information showing exactly where the event loop is blocked and where the blocking code is executing.
import asyncio
import time
from pyleak import EventLoopBlockError, no_event_loop_blocking
async def some_function_with_blocking_code():
print("starting")
time.sleep(1)
print("done")
async def main():
try:
async with no_event_loop_blocking(action="raise"):
await some_function_with_blocking_code()
except EventLoopBlockError as e:
print(e)
if __name__ == "__main__":
asyncio.run(main())
Output:
starting
done
Detected 1 event loop blocks
Event Loop Block: block-1
Duration: 0.605s (threshold: 0.200s)
Timestamp: 1749051796.302
Blocking Stack:
File "/private/tmp/example.py", line 22, in <module>
asyncio.run(main())
File "/opt/homebrew/.../asyncio/runners.py", line 194, in run
return runner.run(main)
File "/opt/homebrew/.../asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/opt/homebrew/.../asyncio/base_events.py", line 671, in run_until_complete
self.run_forever()
File "/opt/homebrew/.../asyncio/base_events.py", line 638, in run_forever
self._run_once()
File "/opt/homebrew/.../asyncio/base_events.py", line 1971, in _run_once
handle._run()
File "/opt/homebrew/.../asyncio/events.py", line 84, in _run
self._context.run(self._callback, *self._args)
File "/private/tmp/example.py", line 16, in main
await some_function_with_blocking_code()
File "/private/tmp/example.py", line 9, in some_function_with_blocking_code
time.sleep(1)
Actions
Control what happens when leaks/blocking are detected:
| Action | AsyncIO Tasks | Threads | Event Loop Blocking |
|--------|---------------|---------|-------------------|
| "warn" (default) | ✅ Issues ResourceWarning | ✅ Issues ResourceWarning | ✅ Issues ResourceWarning |
| "log" | ✅ Writes to logger | ✅ Writes to logger | ✅ Writes to logger |
| "cancel" | ✅ Cancels leaked tasks | ❌ Warns (can't force-stop) | ❌ Warns (can't cancel) |
| "raise" | ✅ Raises TaskLeakError | ✅ Raises ThreadLeakError | ✅ Raises EventLoopBlockError |
# Examples
async with no_task_leaks(action="cancel"): # Cancels leaked tasks
pass
with no_thread_leaks(action="raise"): # Raises exception on thread leaks
pass
async with no_event_loop_blocking(action="log"): # Logs blocking events
pass
Name Filtering
Filter detection by resource names (tasks and threads only):
import re
# Exact match
async with no_task_leaks(name_filter="background-worker"):
pass
with no_thread_leaks(name_filter="worker-thread"):
pass
# Regex pattern
async with no_task_leaks(name_filter=re.compile(r"worker-\d+")):
pass
with no_thread_leaks(name_filter=re.compile(r"background-.*")):
pass
Note: Event loop blocking detection doesn't support name filtering.
Configuration Options
AsyncIO Tasks
no_task_leaks(
action="warn", # Action to take on detection
name_filter=None, # Filter by task name
logger=None # Custom logger
)
Threads
no_thread_leaks(
action="warn", # Action to take on detection
name_filter=None, # Filter by thread name
logger=None, # Custom logger
exclude_daemon=True, # Exclude daemon threads
)
Event Loop Blocking
no_event_loop_blocking(
action="warn", # Action to take on detection
logger=None, # Custom logger
threshold=0.1, # Minimum blocking time to report (seconds)
check_interval=0.01 # How often to check (seconds)
)
Testing
Perfect for catching issues in tests:
import pytest
from pyleak import no_task_leaks, no_thread_leaks, no_event_loop_blocking
@pytest.mark.asyncio
async def test_no_leaked_tasks():
async with no_task_leaks(action="raise"):
await my_async_function()
def test_no_leaked_threads():
with no_thread_leaks(action="raise"):
my_threaded_function()
@pytest.mark.asyncio
async def test_no_event_loop_blocking():
async with no_event_loop_blocking(action="raise", threshold=0.1):
await my_potentially_blocking_function()
Real-World Examples
Detecting Synchronous HTTP Calls in Async Code
import httpx
from starlette.testclient import TestClient
async def test_sync_vs_async_http():
# This will detect blocking
async with no_event_loop_blocking(action="warn"):
response = TestClient(app).get("/endpoint") # Synchronous!
# This will not detect blocking
async with no_event_loop_blocking(action="warn"):
async with httpx.AsyncClient() as client:
response = await client.get("/endpoint") # Asynchronous!
Ensuring Proper Resource Cleanup
async def test_background_task_cleanup():
async with no_task_leaks(action="raise"):
# This would fail the test
asyncio.create_task(lo
