Skip to content

Add atexit handlers to flush callbacks for async completions - #16487

Merged
1 commit merged into
BerriAI:mainfrom
andrewm4894:fix/async-callback-atexit-handler
Nov 12, 2025
Merged

1 commit merged into
BerriAI:mainfrom
andrewm4894:fix/async-callback-atexit-handler

Conversation

@andrewm4894

@andrewm4894 andrewm4894 commented Nov 11, 2025 •

Copy link
Copy Markdown
Contributor

Title

Add atexit handlers to flush callbacks for async completions

Relevant issues

Fixes #16486

Pre-Submission checklist

  • I have Added testing in the tests/litellm/ directory, Adding at least 1 test is a hard requirement
  • I have added a screenshot of my new test passing locally
  • 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

Type

Bug Fix

Changes

Problem

Callbacks configured via litellm.success_callback were not invoked for litellm.acompletion() in short-lived scripts. Callbacks worked correctly for sync completions but async completions queued callbacks that were lost when the script exited before GLOBAL_LOGGING_WORKER could process them.

Solution

Implemented two-level atexit handler approach:

  1. GLOBAL_LOGGING_WORKER atexit handler processes remaining callbacks before exit
  2. Integration-specific atexit handlers flush internal queues to external services

Files Modified

  • litellm/litellm_core_utils/logging_worker.py: Added _flush_on_exit() method and atexit registration
  • litellm/integrations/posthog.py: Added _flush_on_exit() method and atexit registration
  • tests/logging_callback_tests/test_posthog.py: Added 3 new tests verifying atexit handlers

Testing

All 14 tests in test_posthog.py pass. Manual end-to-end testing confirms async events now arrive in PostHog while sync events continue working without regression.

Screenshot of new related tests passing locally:

image

Impact

Fixes async callback delivery for all integrations using GLOBAL_LOGGING_WORKER. No breaking changes, sync path unchanged.

Fixes BerriAI#16486

## Problem
Callbacks configured via litellm.success_callback (e.g., PostHog, LangSmith)
were not being invoked for litellm.acompletion() in short-lived scripts.
The callbacks worked correctly for synchronous completions but async
completions would queue callbacks that were lost when the script exited
before GLOBAL_LOGGING_WORKER could process them.

Root cause: asyncio.run() closes the event loop immediately after the
async function completes, preventing the background worker from processing
queued callbacks.

## Solution
Implemented a two-level atexit handler approach:

1. GLOBAL_LOGGING_WORKER atexit handler (logging_worker.py):
   - Processes remaining callbacks from queue before exit
   - Creates new event loop to run pending coroutines synchronously
   - Applies time and iteration limits to prevent blocking shutdown

2. Integration-specific atexit handlers (posthog.py as example):
   - Flushes internal queue to external service
   - Uses synchronous HTTP client for reliable delivery
   - Each integration needs its own handler due to varying sync APIs

## Changes
- litellm/litellm_core_utils/logging_worker.py:
  - Added _flush_on_exit() method
  - Registered atexit handler in __init__
  - Processes up to MAX_ITERATIONS_TO_CLEAR_QUEUE events
  - Time-limited to MAX_TIME_TO_CLEAR_QUEUE seconds

- litellm/integrations/posthog.py:
  - Added _flush_on_exit() method
  - Registered atexit handler in __init__
  - Groups events by credentials for batch sending
  - Uses sync_client for reliable HTTP delivery

- tests/logging_callback_tests/test_posthog.py:
  - Added test_async_callback_atexit_handler_exists()
  - Added test_posthog_atexit_flushes_internal_queue()
  - Added test_sync_callback_not_affected_by_atexit()

## Testing
- All existing tests pass
- Manual end-to-end testing confirms fix:
  - Async events now arrive in PostHog
  - Sync events continue working (no regression)
- Unit tests verify atexit handlers registered and functional

## Impact
- Fixes async callback delivery for ALL integrations using GLOBAL_LOGGING_WORKER
- No breaking changes - only adds missing functionality
- Sync path unchanged - no performance impact
@vercel

vercel Bot commented Nov 11, 2025

Copy link
Copy Markdown

@andrewm4894 is attempting to deploy a commit to the CLERKIEAI Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant

CLAassistant commented Nov 11, 2025 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ghost
ghost merged commit bd15250 into BerriAI:main Nov 12, 2025
4 of 6 checks passed
ishaan-jaff added a commit that referenced this pull request Nov 15, 2025
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…rriAI#16487)

Fixes BerriAI#16486

## Problem
Callbacks configured via litellm.success_callback (e.g., PostHog, LangSmith)
were not being invoked for litellm.acompletion() in short-lived scripts.
The callbacks worked correctly for synchronous completions but async
completions would queue callbacks that were lost when the script exited
before GLOBAL_LOGGING_WORKER could process them.

Root cause: asyncio.run() closes the event loop immediately after the
async function completes, preventing the background worker from processing
queued callbacks.

## Solution
Implemented a two-level atexit handler approach:

1. GLOBAL_LOGGING_WORKER atexit handler (logging_worker.py):
   - Processes remaining callbacks from queue before exit
   - Creates new event loop to run pending coroutines synchronously
   - Applies time and iteration limits to prevent blocking shutdown

2. Integration-specific atexit handlers (posthog.py as example):
   - Flushes internal queue to external service
   - Uses synchronous HTTP client for reliable delivery
   - Each integration needs its own handler due to varying sync APIs

## Changes
- litellm/litellm_core_utils/logging_worker.py:
  - Added _flush_on_exit() method
  - Registered atexit handler in __init__
  - Processes up to MAX_ITERATIONS_TO_CLEAR_QUEUE events
  - Time-limited to MAX_TIME_TO_CLEAR_QUEUE seconds

- litellm/integrations/posthog.py:
  - Added _flush_on_exit() method
  - Registered atexit handler in __init__
  - Groups events by credentials for batch sending
  - Uses sync_client for reliable HTTP delivery

- tests/logging_callback_tests/test_posthog.py:
  - Added test_async_callback_atexit_handler_exists()
  - Added test_posthog_atexit_flushes_internal_queue()
  - Added test_sync_callback_not_affected_by_atexit()

## Testing
- All existing tests pass
- Manual end-to-end testing confirms fix:
  - Async events now arrive in PostHog
  - Sync events continue working (no regression)
- Unit tests verify atexit handlers registered and functional

## Impact
- Fixes async callback delivery for ALL integrations using GLOBAL_LOGGING_WORKER
- No breaking changes - only adds missing functionality
- Sync path unchanged - no performance impact
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Async callbacks not invoked for acompletion() in short-lived scripts

2 participants