◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 5 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 5 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
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 API250Function Calling17Parallel Function CallingProduction33Agent ArchitecturePython51Concurrency

✦ 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-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.
◈ API / SDK2026-09-22
When Function Calls Turn Into Plain Text, Suspect the Property Names in Your Tool Declarations
A record of chasing MALFORMED_FUNCTION_CALL after output limits and schema conflicts were ruled out, bisecting nine tool declarations down to one property name, and adding a linter that stops bad names before they ship.