#23298·llama_index

Bug: several query engines' _aquery() silently delegates to synchronous _query(), blocking the event loop

Author: alwaysprince05Created Sep 28, 2026Updated Sep 28, 2026

Bug Description

While working on SQLJoinQueryEngine (#22400), I noticed several other query engines have async def _aquery() implementations that simply call the synchronous _query() — so aquery() is async in name only and every step runs blocking work directly on the event loop:

  • retry_query_engine.py — RetryQueryEngine._aquery (~line 73) and RetrySourceQueryEngine._aquery (~line 144): return self._query(query_bundle)
  • retry_source_query_engine.py — RetrySourceQueryEngine._aquery (~line 90): same pattern
  • flare/base.py — FLAREQueryEngine._aquery (~line 259): same pattern (retrieval + LLM generation all sync)
  • router_query_engine.py — ToolRetrieverRouterQueryEngine._aquery (~line 312): same pattern
  • graph_query_engine.py — GraphQueryEngine._aquery (~line 53): calls sync _query_index(...)
  • cogniswitch_query_engine.py — CogniswitchQueryEngine._aquery (~line 61): calls sync query_knowledge()

Meanwhile SubQuestionQueryEngine shows the intended pattern (separate _query_subq / _aquery_subq, see sub_question_query_engine.py), and SQLJoinQueryEngine is being fixed via asyncio.to_thread in #22400.

Impact

Same class of problem as the SQLJoinQueryEngine case: applications calling these engines' aquery() from an async context (e.g. FastAPI) get sequential synchronous LLM/retrieval calls on the event loop, serializing concurrent requests and risking timeouts.

Proposed fix

For engines whose internals are fully synchronous, offload with asyncio.to_thread(self._query, query_bundle) (same approach as #22400). Where async primitives exist (retriever aretrieve, LLM achat), prefer a true async path.

Happy to open PRs for these — would maintainers prefer one PR per engine, or a single core PR covering all of them?