When a sync FastAPI route handler (running in uvicorn's threadpool) needs to call async Redis/Valkey operations, the common pattern of asyncio.new_event_loop().run_until_complete(async_fn()) silently fails when the async Redis client was created and bound to uvicorn's main event loop.
The async redis.asyncio.Redis client's connection pool is tied to the event loop that created it. Creating a new event loop in a threadpool worker creates a separate event loop that cannot reuse the main loop's connection pool. Operations appear to execute but connections silently fail or hang, and broad except Exception blocks swallow the errors.
try/except around asyncio.new_event_loop() catches everythingCreate a separate sync redis.Redis client alongside the async one at startup. Use it directly in sync code paths — no event loop gymnastics needed.
# At startup (lifespan)
import redis as sync_redis
sync_client = sync_redis.from_url(valkey_url, decode_responses=True)
init_rate_limiter(async_client, sync_client)
# Module-level singleton
_sync_valkey_client: Any | None = None
def _get_sync_valkey():
from gtsrv.ratelimit import _sync_valkey_client
return _sync_valkey_client
# In sync endpoint — direct sync calls, no asyncio
def authorize_device_code(...):
valkey = _get_sync_valkey()
valkey.set(key, json.dumps(record), ex=ttl)This eliminates the event loop mismatch entirely. The sync client is lightweight (only used for rare OAuth callbacks) and doesn't need retry configuration. Keep the async client for all async endpoints (rate limiting, polling, etc.).