GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-04-23Advanced

Parallel Function Calling in Gemini API: Production Patterns, Pitfalls, and Monitoring

A production guide to Parallel Function Calling in the Gemini API: DAG tool design, partial failure handling, rate limits, and monitoring — with working code.

Gemini API192Function Calling16Parallel Function CallingProduction32Agent ArchitecturePython38Concurrency2

Premium Article

If you have ever built a tool-using agent on top of the Gemini API, you have probably felt the slowness creep in. The more tools you wire up, the longer the user waits after pressing Enter. When I first shipped a research agent that called search, math, RAG, and an external API in sequence, the full round trip took well over ten seconds.

Parallel Function Calling is the Gemini API feature that lets you skip that pain. The model can return multiple functionCall parts in a single turn, and your application can dispatch them in parallel before sending the results back. Done right, you spend one model round-trip instead of three or four, and the user sees answers in seconds rather than teenagers-refreshing-Instagram time.

This article goes beyond the happy path. We cover the spec, why "just parallelize everything" breaks in production, and the design patterns I rely on for dependency graphs, partial failures, rate limits, and observability — all with working code you can adapt. If you have tried to parallelize and ended up with flaky agents, you should find something concrete to take back to your codebase.

What Parallel Function Calling Actually Is

In the Gemini API, Parallel Function Calling means the model can emit several functionCall parts inside a single candidates[0].content:

{
  "candidates": [{
    "content": {
      "parts": [
        { "functionCall": { "name": "search_orders",  "args": { "user_id": "u_123" }}},
        { "functionCall": { "name": "get_inventory",  "args": { "sku": "ABC-001" }}},
        { "functionCall": { "name": "fetch_shipping", "args": { "zip": "150-0001" }}}
      ]
    }
  }]
}

Because these three calls are independent, your app can run them concurrently with asyncio.gather or Promise.all. You go from three model round-trips (each adding 200–800 ms of network latency) down to one.

Whether the model actually decides to parallelize depends on the tool signatures, the conversation history, and your toolConfig. In practice it tends to fall back to sequential calls when:

  • Tool B needs the result of Tool A as an argument
  • toolConfig.function_calling_config.mode is set to ANY with allowed_function_names narrowed to one tool
  • Your prompt says something like "First call A, then decide whether to call B"

To coax the model into parallelizing, write tool descriptions that explicitly state which tools are independent, or that the same information is best gathered from multiple sources simultaneously. I show the exact wording I use later in this article.

A Minimal Implementation in Python

Let's start with a small working example. This uses the new google-genai SDK and simulates a support agent that answers "What's the status of my order, and when will it arrive?" by fetching order history, inventory, and shipping info in parallel.

# pip install google-genai httpx tenacity
import asyncio
import os
from typing import Any
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["YOUR_GEMINI_API_KEY"])
 
# --- Tool declarations ---
def declare_tools() -> list[types.Tool]:
    return [
        types.Tool(function_declarations=[
            types.FunctionDeclaration(
                name="search_orders",
                description=(
                    "Look up a user's orders from the last 3 months. "
                    "Safe to call in parallel with get_inventory and fetch_shipping."
                ),
                parameters=types.Schema(
                    type="OBJECT",
                    properties={"user_id": types.Schema(type="STRING")},
                    required=["user_id"],
                ),
            ),
            types.FunctionDeclaration(
                name="get_inventory",
                description=(
                    "Return the current stock count for a SKU. "
                    "Safe to call in parallel with search_orders and fetch_shipping."
                ),
                parameters=types.Schema(
                    type="OBJECT",
                    properties={"sku": types.Schema(type="STRING")},
                    required=["sku"],
                ),
            ),
            types.FunctionDeclaration(
                name="fetch_shipping",
                description=(
                    "Return shipping ETA and available carriers for a postal code. "
                    "Safe to call in parallel with search_orders and get_inventory."
                ),
                parameters=types.Schema(
                    type="OBJECT",
                    properties={"zip": types.Schema(type="STRING")},
                    required=["zip"],
                ),
            ),
        ])
    ]
 
# --- Tool implementations (replace with real DB / API calls) ---
async def search_orders(user_id: str) -> dict[str, Any]:
    await asyncio.sleep(0.3)  # simulated DB round-trip
    return {"orders": [{"id": "o_9001", "sku": "ABC-001", "status": "shipped"}]}
 
async def get_inventory(sku: str) -> dict[str, Any]:
    await asyncio.sleep(0.2)
    return {"sku": sku, "stock": 42}
 
async def fetch_shipping(zip: str) -> dict[str, Any]:
    await asyncio.sleep(0.4)
    return {"zip": zip, "carriers": ["JP Post", "Yamato"], "eta_days": 2}
 
TOOL_REGISTRY = {
    "search_orders": search_orders,
    "get_inventory": get_inventory,
    "fetch_shipping": fetch_shipping,
}
 
# --- Parallel dispatcher ---
async def run_parallel_tools(function_calls: list[types.FunctionCall]) -> list[types.Part]:
    async def dispatch(fc: types.FunctionCall) -> types.Part:
        impl = TOOL_REGISTRY.get(fc.name)
        if impl is None:
            result = {"error": f"unknown tool: {fc.name}"}
        else:
            try:
                result = await impl(**(fc.args or {}))
            except Exception as exc:
                # Never let one failing tool bring down the whole batch.
                result = {"error": str(exc), "tool": fc.name}
        return types.Part.from_function_response(name=fc.name, response=result)
 
    return await asyncio.gather(*(dispatch(fc) for fc in function_calls))
 
async def agent_turn(user_message: str) -> str:
    tools = declare_tools()
    history: list[types.Content] = [
        types.Content(role="user", parts=[types.Part.from_text(user_message)])
    ]
 
    for _ in range(4):  # max 4 turns
        resp = client.models.generate_content(
            model="gemini-2.5-pro",
            contents=history,
            config=types.GenerateContentConfig(tools=tools, temperature=0.2),
        )
        parts = resp.candidates[0].content.parts
        fcalls = [p.function_call for p in parts if p.function_call]
        if not fcalls:
            return "".join(p.text or "" for p in parts)
 
        history.append(types.Content(role="model", parts=parts))
        tool_parts = await run_parallel_tools(fcalls)
        history.append(types.Content(role="user", parts=tool_parts))
 
    return "Conversation looped without convergence. Escalating to a human."
 
if __name__ == "__main__":
    msg = (
        "User u_123 wants the status of SKU ABC-001, "
        "and shipping info for postal code 150-0001."
    )
    print(asyncio.run(agent_turn(msg)))

With this setup, Gemini will usually emit all three calls in the first turn. The dispatcher runs them concurrently, so the total tool latency is driven by the slowest one (~400 ms), not their sum (~900 ms). You will see the difference in your logs immediately.

Why it's written this way

I always wrap each tool in try / except and return {"error": ...} on failure. Raising out of asyncio.gather is convenient for quick scripts, but in an agent you want the model to see which tool failed and decide how to recover, not die with a 500. Making every tool return a response object — even an error one — keeps the downstream prompt logic simple.

I use asyncio.gather instead of thread pools because most tools end up calling async HTTP clients (httpx.AsyncClient) or async DB drivers. Mixing sync blocking code into an otherwise async agent is a common source of subtle slowdowns.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
If your Function-Calling agent feels sluggish because tools run one after another, this guide shows how to switch to parallel execution and cut perceived latency by 2–4x
You will learn the failure modes unique to parallel tool calls — partial failures, ordering, and race conditions — with code patterns that are safe for production
You can build support and data-analysis agents that stay within SLA latency budgets, with a monitoring story that actually catches regressions
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

API / SDK2026-05-31
Bulk Processing Without the 429s: Adaptive Concurrency for the Gemini API
Pushing tens of thousands of requests through the Gemini API with a fixed concurrency almost always produces 429s and dropped items. Here is an AIMD design that auto-tunes concurrency from the 429 feedback, with a bounded worker pool, a dead-letter queue, and resumable checkpoints.
API / SDK2026-05-24
Why Your Gemini File API Uploads Vanish After 48 Hours — and How to Code Around It
Gemini File API resources are auto-deleted 48 hours after upload. Here is how to recognize the failure, why it happens, and concrete patterns for re-uploading, falling back to inline data, and managing expiration safely.
API / SDK2026-05-04
Implementing Structured Output with Gemini Function Calling — Multi-Tool Design Patterns
A practical guide to reliable structured output with Gemini API Function Calling — covering tool definition best practices, multi-tool coordination, and error handling.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →