BigQuery execute_sql blocks the event loop for the whole query

Open
#7,160 2 comments 0 reactions 1 assignee View on GitHub

@surajksharma07 is already working on this.

Since Sep 17, 2026.

Assessment

This issue has not been assessed yet.

Description

request clarification tools

🔴 Required Information

Describe the Bug:
execute_sql in the BigQuery toolset is a plain def, and the BigQuery client it calls blocks (src/google/adk/integrations/bigquery/query_tool.py, L341 and the _execute_sql helper it delegates to at L169). ADK awaits a sync tool inline on the running loop, so nothing else in the process runs until the query comes back. On a server handling more than one session, the others just wait.

Spanner hit the same thing and was fixed in 1dbceccf. Bigtable's execute_sql is async too. BigQuery still has no asyncio.to_thread anywhere.

Steps to Reproduce:

  1. uv pip install "google-adk[gcp]==2.9.0"
  2. Save the script below (also under Minimal Reproduction Code) as repro.py. It replaces the BigQuery client with one that takes 2 seconds, so no credentials or real dataset are needed, and counts how many times a 100 ms heartbeat task gets to run during the query.
  3. python repro.py

Expected Behavior:
The heartbeat keeps ticking every 100 ms while the query is in flight.

Observed Behavior:
It doesn't tick at all until the query returns:

heartbeat ran 3 times
longest gap between heartbeats: 2.108s

The gap is the query duration.

Environment Details:

  • ADK Library Version (pip show google-adk): 2.9.0. Also checked main at 7ae1c9b0, still sync there.
  • Desktop OS: macOS 25.6.0 (arm64)
  • Python Version (python -V): 3.11.12, in a uv venv set up per the contribution guide (uv venv --python "python3.11" + uv sync --all-extras)

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: N/A, no model involved

🟡 Optional Information

Regression:
No, it has always been sync.

Additional Context:
Same fix as Spanner: make execute_sql a coroutine and push _execute_sql into a thread. _execute_sql has to stay sync because forecast, analyze_contribution and detect_anomalies call it directly. Those three block the loop as well, but that's a separate change.

I have a patch and tests ready, so please assign this to me.

Minimal Reproduction Code:

import asyncio
import time
from unittest import mock

from google.adk.integrations.bigquery import query_tool
from google.adk.integrations.bigquery.config import BigQueryToolConfig
from google.adk.tools.tool_context import ToolContext
from google.auth.credentials import Credentials
from google.cloud import bigquery

QUERY_SECONDS = 2.0


async def heartbeat(stop):
    ticks = []
    start = time.monotonic()
    while not stop.is_set():
        await asyncio.sleep(0.1)
        ticks.append(time.monotonic() - start)
    return ticks


def slow_query_and_wait(*args, **kwargs):
    time.sleep(QUERY_SECONDS)
    return [{"num": 123}]


async def main():
    credentials = mock.create_autospec(Credentials, instance=True)
    tool_context = mock.create_autospec(ToolContext, instance=True)
    settings = BigQueryToolConfig()

    with mock.patch.object(bigquery, "Client", autospec=True) as client:
        bq_client = client.return_value
        query_job = mock.create_autospec(bigquery.QueryJob)
        query_job.statement_type = "SELECT"
        bq_client.query.return_value = query_job
        bq_client.query_and_wait.side_effect = slow_query_and_wait

        stop = asyncio.Event()
        beat = asyncio.create_task(heartbeat(stop))
        await asyncio.sleep(0.3)

        result = query_tool.execute_sql(
            "my_project", "SELECT 123 AS num", credentials, settings,
            tool_context,
        )
        if asyncio.iscoroutine(result):
            result = await result

        stop.set()
        ticks = await beat

    gaps = [b - a for a, b in zip(ticks, ticks[1:])]
    print(f"heartbeat ran {len(ticks)} times")
    print(f"longest gap between heartbeats: {max(gaps):.3f}s")
    print(f"result: {result}")


asyncio.run(main())

How often has this issue occurred?:

  • Always (100%)
Dominant language
Python
Stars
21.6k
Forks
4k
Avg merge
13h 49m
Merged PRs (30d)
10

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/adk-python

All issues in google/adk-python

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.