Main Ecosystems
HomePython CorePandas ReferenceNumPy ScientificFastAPI & PydanticDjango Enterprise
More Ecosystems
Environment & SetupRequests & HTTPAsyncIO ConcurrencyObject-Oriented OOPPyTorch Deep LearningScikit-Learn MLFlask FrameworkWeb ScrapingDatabase & ORMDevOps & Docker

Asyncio/Discord.py: Fixing Unclosed Client Session and ResourceWarning

Verified FixPython 3.10+asyncio (Python Native), aiohttp 3.9+, discord.py 2.3+Silo: asyncio

Quick Fix / Solution Rapide

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)

Example: Bug Reproduction
# 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.

Example: Recommended Solution
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.

Example: Alternative Solution
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.