litellm - ✅(Solved) Fix [Bug]: "Fatal error on SSL transport" at process exit after acompletion with asyncio.gather [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#23278Fetched 2026-04-08 00:37:47
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Timeline (top)
labeled ×3commented ×1cross-referenced ×1referenced ×1

Error Message

Zero One Two Three. Four ERROR:asyncio:Fatal error on SSL transport protocol: <asyncio.sslproto.SSLProtocol object at 0x10d63af80> transport: <_SelectorSocketTransport closing fd=9> Traceback (most recent call last): File ".../asyncio/selector_events.py", line 924, in write n = self._sock.send(data) OSError: [Errno 9] Bad file descriptor

During handling of the above exception, another exception occurred:

Traceback (most recent call last): File ".../asyncio/sslproto.py", line 690, in _process_write_backlog self._transport.write(chunk) File ".../asyncio/selector_events.py", line 930, in write self._fatal_error(exc, 'Fatal write error on socket transport') File ".../asyncio/selector_events.py", line 725, in _fatal_error self._force_close(exc) File ".../asyncio/selector_events.py", line 737, in _force_close self._loop.call_soon(self._call_connection_lost, exc) File ".../asyncio/base_events.py", line 753, in call_soon self._check_closed() File ".../asyncio/base_events.py", line 515, in _check_closed raise RuntimeError('Event loop is closed') RuntimeError: Event loop is closed

(repeated once per concurrent request — 5 times with the script above)

Fix Action

Fixed

PR fix notes

PR #23289: fix: register async client cleanup eagerly to prevent SSL exit errors.

Description (problem / solution / changelog)

Closes #23278

Relevant issues

<!-- e.g. "Fixes #000" -->

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Type

<!-- Select the type of Pull Request --> <!-- Keep only the necessary ones -->

🆕 New Feature 🐛 Bug Fix 🧹 Refactoring 📖 Documentation 🚄 Infrastructure ✅ Test

Changes

Changed files

  • litellm/__init__.py (modified, +11/-2)
  • tests/test_litellm/llms/custom_httpx/test_gemini_session_leak.py (modified, +19/-0)

Code Example

import asyncio
from litellm import acompletion


async def main():
    responses = await asyncio.gather(
        *[
            acompletion(
                model="gemini/gemini-2.5-flash-lite",
                messages=[{"role": "user", "content": f"Say the number {i}"}],
            )
            for i in range(5)
        ]
    )
    for r in responses:
        print(r.choices[0].message.content)


asyncio.run(main())

---

Zero
One
Two
Three.
Four
ERROR:asyncio:Fatal error on SSL transport
protocol: <asyncio.sslproto.SSLProtocol object at 0x10d63af80>
transport: <_SelectorSocketTransport closing fd=9>
Traceback (most recent call last):
  File ".../asyncio/selector_events.py", line 924, in write
    n = self._sock.send(data)
OSError: [Errno 9] Bad file descriptor

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../asyncio/sslproto.py", line 690, in _process_write_backlog
    self._transport.write(chunk)
  File ".../asyncio/selector_events.py", line 930, in write
    self._fatal_error(exc, 'Fatal write error on socket transport')
  File ".../asyncio/selector_events.py", line 725, in _fatal_error
    self._force_close(exc)
  File ".../asyncio/selector_events.py", line 737, in _force_close
    self._loop.call_soon(self._call_connection_lost, exc)
  File ".../asyncio/base_events.py", line 753, in call_soon
    self._check_closed()
  File ".../asyncio/base_events.py", line 515, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed


(repeated once per concurrent request — 5 times with the script above)
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

After acompletion() calls finish successfully, the process prints multiple ERROR:asyncio:Fatal error on SSL transport tracebacks on exit. The errors are harmless (exit code is 0) but pollute stderr, which is confusing for users and noisy in CI logs.

The issue reproduces reliably when multiple concurrent requests are made via asyncio.gather(). Single acompletion() calls also reproduce but less consistently.

Expected: Clean exit with no error output after successful completions.

Steps to Reproduce

  1. Set GEMINI_API_KEY environment variable
  2. Run the following script:
import asyncio
from litellm import acompletion


async def main():
    responses = await asyncio.gather(
        *[
            acompletion(
                model="gemini/gemini-2.5-flash-lite",
                messages=[{"role": "user", "content": f"Say the number {i}"}],
            )
            for i in range(5)
        ]
    )
    for r in responses:
        print(r.choices[0].message.content)


asyncio.run(main())
  1. Observe the SSL transport errors printed after the output.

Relevant log output

Zero
One
Two
Three.
Four
ERROR:asyncio:Fatal error on SSL transport
protocol: <asyncio.sslproto.SSLProtocol object at 0x10d63af80>
transport: <_SelectorSocketTransport closing fd=9>
Traceback (most recent call last):
  File ".../asyncio/selector_events.py", line 924, in write
    n = self._sock.send(data)
OSError: [Errno 9] Bad file descriptor

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../asyncio/sslproto.py", line 690, in _process_write_backlog
    self._transport.write(chunk)
  File ".../asyncio/selector_events.py", line 930, in write
    self._fatal_error(exc, 'Fatal write error on socket transport')
  File ".../asyncio/selector_events.py", line 725, in _fatal_error
    self._force_close(exc)
  File ".../asyncio/selector_events.py", line 737, in _force_close
    self._loop.call_soon(self._call_connection_lost, exc)
  File ".../asyncio/base_events.py", line 753, in call_soon
    self._check_closed()
  File ".../asyncio/base_events.py", line 515, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed


(repeated once per concurrent request — 5 times with the script above)

What part of LiteLLM is this about?

SDK (litellm Python package)

What LiteLLM version are you on ?

1.82.1

Twitter / LinkedIn details

https://www.linkedin.com/in/arkadiusz-kwasigroch/

extent analysis

Fix Plan

To fix the ERROR:asyncio:Fatal error on SSL transport issue, we need to ensure that the SSL transports are properly closed before the event loop is closed.

Here are the steps to fix the issue:

  • Use asyncio.run() with the debug parameter set to True to get more detailed error messages.
  • Use a try-except block to catch the RuntimeError exception and ignore it.
  • Use the loop.close() method to close the event loop after all tasks have completed.

However, a more straightforward solution is to use asyncio.gather() with the return_exceptions parameter set to True to prevent the exception from being raised.

Here's an example code snippet that demonstrates the fix:

import asyncio
from litellm import acompletion

async def main():
    try:
        responses = await asyncio.gather(
            *[
                acompletion(
                    model="gemini/gemini-2.5-flash-lite",
                    messages=[{"role": "user", "content": f"Say the number {i}"}],
                )
                for i in range(5)
            ],
            return_exceptions=True  # Add this parameter
        )
        for r in responses:
            if isinstance(r, Exception):
                print(f"Error: {r}")
            else:
                print(r.choices[0].message.content)
    except Exception as e:
        print(f"Error: {e}")

asyncio.run(main())

Verification

To verify that the fix worked, run the modified script and check that there are no ERROR:asyncio:Fatal error on SSL transport messages printed to the console.

Extra Tips

  • Make sure to handle exceptions properly in your asynchronous code to prevent unexpected errors.
  • Use the return_exceptions parameter with asyncio.gather() to prevent exceptions from being raised and to handle them instead.
  • Consider using a more robust error handling mechanism, such as logging exceptions and providing user-friendly error messages.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: "Fatal error on SSL transport" at process exit after acompletion with asyncio.gather [1 pull requests, 1 comments, 2 participants]