Skip to content

Fix: sync requests blocking event loop in async webhook/auth handlers#5432

Open
atlas-agent-omi[bot] wants to merge 1 commit intomainfrom
atlas/fix-sync-requests-in-async-webhooks
Open

Fix: sync requests blocking event loop in async webhook/auth handlers#5432
atlas-agent-omi[bot] wants to merge 1 commit intomainfrom
atlas/fix-sync-requests-in-async-webhooks

Conversation

@atlas-agent-omi
Copy link

Problem

Fixes #5431

6 synchronous requests.post()/requests.get() calls inside async def functions block the asyncio event loop for up to 15 seconds each. This stalls all concurrent WebSocket connections and HTTP handlers while waiting for external endpoints to respond.

Critical path (webhooks — called during real-time transcription):

  • realtime_transcript_webhook() — called on every transcript segment
  • send_audio_bytes_developer_webhook() — called for audio streaming

Lower priority (auth — one-time flows):

  • _exchange_google_code_for_oauth_credentials() — Google token exchange
  • _exchange_apple_code_for_oauth_credentials() — Apple token exchange
  • _generate_custom_token() — Firebase sign-in
  • oauth_token() — external app setup check

Fix

Wrapped all sync requests calls with asyncio.to_thread() to offload to the thread pool:

# Before (blocks event loop):
response = requests.post(url, json=data, timeout=15)

# After (non-blocking):
response = await asyncio.to_thread(requests.post, url, json=data, timeout=15)

Files Changed

  • backend/utils/webhooks.py — 2 calls fixed
  • backend/routers/auth.py — 3 calls fixed
  • backend/routers/oauth.py — 1 call fixed

Testing

  • No API changes — drop-in replacement
  • asyncio.to_thread() is stdlib (Python 3.9+), no new dependencies
  • Behavior is identical, just non-blocking

Use asyncio.to_thread() to offload synchronous requests.post/get calls
to the thread pool in async webhook, auth, and oauth handlers.

Previously these sync calls would block the entire asyncio event loop
for up to 15 seconds per call, stalling all concurrent connections.

Fixes #5431
@atlas-agent-omi atlas-agent-omi bot requested a review from kodjima33 March 7, 2026 23:32
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR correctly addresses the core concurrency problem: 6 synchronous requests calls inside async def functions were blocking the asyncio event loop, potentially stalling all concurrent WebSocket connections and HTTP handlers for up to 15 seconds per call. The fix — wrapping each call with asyncio.to_thread() — is the correct and idiomatic approach for offloading blocking I/O to the thread pool without changing external behavior.

Key changes:

  • backend/utils/webhooks.py: realtime_transcript_webhook and send_audio_bytes_developer_webhook correctly converted to non-blocking — these are the highest-impact fixes since they fire on every real-time transcript segment and audio stream.
  • backend/routers/auth.py: Google token exchange, Apple token exchange, and Firebase sign-in calls correctly converted — lower frequency (one-time auth flows) but still appropriate.
  • backend/routers/oauth.py: App setup-completion check correctly converted with proper exception handling.

Minor note: The three converted requests.post calls in auth.py lack timeout parameters. While the event-loop blocking is fixed, adding timeouts (as done in webhooks.py) would follow best practices and prevent thread-pool resource exhaustion under slow external endpoints.

Confidence Score: 4/5

  • Safe to merge. The core fix (event-loop blocking via asyncio.to_thread) is correctly implemented across all async handlers.
  • The PR successfully solves the stated problem: all 6 blocking requests calls inside async def functions are now properly offloaded to thread pools, restoring event-loop responsiveness. The implementation is correct and idiomatic. Minor point: three auth.py HTTP calls lack timeout parameters, which is a defensive practice but not a blocker since the event-loop fix is achieved.
  • backend/routers/auth.py (add timeouts to three asyncio.to_thread(requests.post) calls)

Important Files Changed

Filename Overview
backend/utils/webhooks.py Two async webhook functions (realtime_transcript_webhook, send_audio_bytes_developer_webhook) are correctly fixed with asyncio.to_thread to offload blocking HTTP calls. The two sync webhook functions (conversation_created_webhook, day_summary_webhook) contain blocking requests.post calls, but these are already dispatched to separate threads at their call sites in process_conversation.py and notifications.py, so they do not block the event loop.
backend/routers/auth.py Three blocking requests.post calls correctly wrapped with asyncio.to_thread. Minor concern: none of the three converted calls specify a timeout parameter, which is a best-practice defensiveness measure to prevent thread-pool exhaustion under slow external endpoints. Consider aligning with the timeout pattern used in webhooks.py.
backend/routers/oauth.py Single requests.get for setup-completion check correctly wrapped with asyncio.to_thread (line 143). Exception handling for requests.RequestException and ValueError is preserved correctly. No issues.

Sequence Diagram

sequenceDiagram
    participant WS as WebSocket Handler (async)
    participant WH as webhooks.py
    participant TP as Thread Pool (asyncio.to_thread)
    participant EXT as External Webhook URL

    Note over WS,EXT: After fix — realtime_transcript_webhook & send_audio_bytes_developer_webhook
    WS->>WH: await realtime_transcript_webhook(uid, segments)
    WH->>TP: asyncio.to_thread(requests.post, webhook_url, ...)
    Note over WS: Event loop free to handle other connections
    TP-->>EXT: HTTP POST (blocking, in thread)
    EXT-->>TP: 200 OK
    TP-->>WH: response
    WH-->>WS: return
Loading

Last reviewed commit: 1b73b0b

}

token_response = requests.post(token_url, data=token_data)
token_response = await asyncio.to_thread(requests.post, token_url, data=token_data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The three requests.post calls converted to asyncio.to_thread in this file (Google token exchange, Apple token exchange, and Firebase sign-in) lack timeout parameters. While these calls no longer block the event loop directly, they still consume thread-pool slots. Without a timeout, a slow or unresponsive endpoint could hang indefinitely. Consider adding a reasonable timeout (e.g., timeout=10) to match the pattern already established in webhooks.py:

Suggested change
token_response = await asyncio.to_thread(requests.post, token_url, data=token_data)
token_response = await asyncio.to_thread(requests.post, token_url, data=token_data, timeout=10)

The same applies to the Apple token exchange at line 344 and Firebase sign-in at line 412.

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: Sync requests.post in async webhook functions blocks event loop

0 participants