Asyncio/Discord.py: Fixing Unclosed Client Session and ResourceWarning
This error occurs when Python tries to terminate an asyncio event loop while an aiohttp.ClientSession or Discord.py bot instance remains open. Close sessions gracefully with await bot.close() or inside an async context manager.
Root Cause Analysis
This error occurs when Python terminates an asyncio event loop while an underlying aiohttp.ClientSession or Discord.py HTTP connection pool remains open and unclosed.
Root Cause 1: Terminating the Event Loop Before Closing HTTP Sessions
In asynchronous programming with aiohttp (which Discord.py uses for Gateway websockets and REST API interactions), each ClientSession manages active TCP connector pools. When a script exits or stops the event loop (loop.close()) without awaiting session.close() or bot.close(), the garbage collector discovers open network sockets and emits an Unclosed client session ResourceWarning or RuntimeError.
Root Cause 2: KeyboardInterrupt (Ctrl+C) Abrupt Exits
When a developer stops a Discord bot or asyncio daemon by pressing Ctrl+C, Python raises a KeyboardInterrupt exception. If the bot does not handle signals or lack a try...finally block to await bot.close(), execution halts abruptly, leaving client sessions dangling.
Root Cause 3: Creating New Client Sessions Without Closing Them
Instantiating aiohttp.ClientSession() inside async functions or event handlers without using async with aiohttp.ClientSession() as session: leaks sessions every time the function executes.
Root Cause 4: Discord.py Bot Initialization Changes in v2.0+
In Discord.py 2.0+, bot.run() encapsulates event loop management. Running manual loop operations like asyncio.get_event_loop().run_until_complete() alongside bot.start() often leads to uncoordinated teardown cycles.
Reproduction Code (MCVE)
# Simulating unclosed aiohttp ClientSession upon bot shutdown
class MockClientSession:
def __init__(self):
self.closed = False
def check_clean_shutdown(self):
if not self.closed:
raise RuntimeError(
"RuntimeError: Unclosed client session. "
"aiohttp ClientSession was not closed before event loop termination. "
"Call await session.close() or await bot.close()."
)
session = MockClientSession()
session.check_clean_shutdown()
Solution 1: Implement Graceful Shutdown with Async Context Managers
Ensure bot.close() or session.close() is always awaited within a try...finally block or async context manager during shutdown.
import asyncio
# Solution 1: Ensure graceful shutdown in async context manager or finally block
class AsyncBotService:
def __init__(self):
self.is_session_closed = False
async def start(self):
print("Bot service started successfully.")
async def close(self):
# Close internal HTTP sessions cleanly before event loop stops
self.is_session_closed = True
print("Bot client session closed gracefully.")
async def main():
bot = AsyncBotService()
try:
await bot.start()
finally:
await bot.close()
asyncio.run(main())
Solution 2: Cancel Pending Tasks and Await Teardown
Collect and cancel all pending asyncio tasks before closing the event loop to ensure background coroutines finish cleanups.
import asyncio
# Solution 2: Clean up background tasks before loop termination
async def background_worker():
try:
await asyncio.sleep(0.01)
except asyncio.CancelledError:
print("Background worker received cancellation, cleaning up.")
async def run_application():
task = asyncio.create_task(background_worker())
await asyncio.sleep(0.02)
# Cancel pending tasks cleanly
if not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
print("Application teardown complete.")
asyncio.run(run_application())
In modern Python 3.11+, use asyncio.Runner or asyncio.run() which automatically cancels all remaining tasks and shuts down asynchronous generators. Avoid deprecated patterns like loop = asyncio.get_event_loop(); loop.run_forever() which do not perform automatic cleanup.
Another edge case in Discord.py is overriding bot.close() without calling await super().close(). If you override the close method in a custom commands.Bot subclass, you must always call super().close() to ensure the internal aiohttp session is terminated.
Contrast Unclosed client session with Event loop is closed: The former is a warning/error that resources were not cleaned up before loop shutdown, while the latter is a RuntimeError raised when trying to schedule coroutines on a loop that has already terminated.