Introduction & Context

Welcome back! You've mastered sequential workflows with prompt chaining and conditional workflows with intelligent routing. Now it's time to unlock dramatic performance improvements by learning parallel processing — executing multiple independent Gemini API calls simultaneously instead of waiting for each one to complete.

In this lesson, you'll discover how to transform workflows that take minutes into operations that complete in seconds. You'll learn the difference between synchronous and asynchronous programming, master Python's asyncio library, and build a system that asks multiple questions to Gemini at the same time.

Gemini API Configuration (with Environment Variables)

Before we dive in, let's set up the Gemini API using environment variables for credentials and endpoint configuration. This is the recommended approach for security and flexibility.

import os
from google import genai
from google.genai import types

# Set up Gemini API credentials
API = os.environ["GOOGLE_API_KEY"]
BASE = os.environ.get("GOOGLE_BASE_URL", "").rstrip("/")

client_kwargs = {"api_key": API}
if BASE:
    client_kwargs["http_options"] = types.HttpOptions(base_url=BASE)

client = genai.Client(**client_kwargs)

# Choose a model to use
MODEL_NAME = "models/gemini-flash-latest"
The Parallelization Workflow Pattern

Before diving into the technical details, let's understand the high-level pattern we'll be implementing. This workflow has two distinct phases that work together to provide both speed and comprehensive results:

Phase 1: Parallel Research Gathering

  • Launch multiple independent Gemini API calls simultaneously.
  • Each call researches a different aspect of your topic (attractions, transportation, culture).
  • All questions run concurrently, completing in roughly the time of the slowest individual request.
  • Results are collected and preserved in their original order.

Phase 2: Sequential Result Synthesis

  • Combine all parallel research into a single comprehensive dataset.
  • Send the aggregated information to Gemini with instructions for synthesis.
  • Generate a unified, actionable final result (like a complete travel guide).
  • This sequential step ensures all information is properly integrated.

This two-phase approach maximizes both efficiency and quality: you get the speed benefits of parallel processing for data gathering while maintaining coherent analysis through sequential aggregation. It's particularly powerful for research tasks, analysis workflows, and any scenario where you need to quickly gather diverse information and synthesize it into actionable insights.

Understanding Sync vs Async Gemini API Calls

When working with the Gemini API, you typically interact with the Google Gen AI Python SDK (google.genai). The SDK's methods for text generation are synchronous by default, meaning each API call blocks your program until a response is received. This is simple but can be slow if you have many independent tasks.

Here's a basic synchronous Gemini API call using the updated configuration:

import os
from google import genai
from google.genai import types

API = os.environ["GOOGLE_API_KEY"]
BASE = os.environ.get("GOOGLE_BASE_URL", "").rstrip("/")

client_kwargs = {"api_key": API}
if BASE:
    client_kwargs["http_options"] = types.HttpOptions(base_url=BASE)

client = genai.Client(**client_kwargs)

MODEL_NAME = "models/gemini-flash-latest"

# Synchronous call - waits for the response before continuing
response = client.models.generate_content(
    model=MODEL_NAME,
    contents="What are the top 3 must-see attractions in Paris?",
)
print(response.text)

To achieve parallelism, you need to run multiple Gemini API calls concurrently. Since the SDK does not provide native async methods, you can use Python's asyncio library in combination with run_in_executor to execute synchronous Gemini calls in parallel threads. This allows your program to launch multiple requests at once and continue running while waiting for responses.

In summary:

  • Use synchronous calls for simple, sequential workflows where each step depends on the previous one.
  • Use asyncio with thread executors to launch multiple independent Gemini API calls at once, dramatically improving performance for batch or parallel tasks.

Choosing the right execution pattern is the key to optimizing your Gemini workflows for both simplicity and speed.

AsyncIO Fundamentals for Gemini Workflows

Python's asyncio library provides an event loop that manages multiple operations simultaneously, switching between them efficiently rather than blocking on any single operation. Since Gemini's SDK is synchronous, you can use asyncio's run_in_executor to run blocking Gemini API calls in separate threads, allowing for concurrent execution.

Here's how you can wrap a Gemini API call for async execution:

import os
import asyncio
from google import genai
from google.genai import types

API = os.environ["GOOGLE_API_KEY"]
BASE = os.environ.get("GOOGLE_BASE_URL", "").rstrip("/")

client_kwargs = {"api_key": API}
if BASE:
    client_kwargs["http_options"] = types.HttpOptions(base_url=BASE)

client = genai.Client(**client_kwargs)

MODEL_NAME = "models/gemini-flash-latest"

async def your_async_function(question):
    loop = asyncio.get_running_loop()
    # Run the blocking Gemini call in a thread pool executor
    response = await loop.run_in_executor(
        None,  # Use default ThreadPoolExecutor
        lambda: client.models.generate_content(
            model=MODEL_NAME,
            contents=question,
        ),
    )
    return response.text

This approach is particularly effective for I/O-bound operations like API calls, where much of the time is spent waiting for network responses.

Running Async Code with asyncio.run()

To execute async functions, you need an event loop. asyncio.run() creates an event loop, runs your async function, and cleans up afterward. This is the standard entry point for async programs:

async def main():
    # Call your async function and await the result
    result = await your_async_function("What are the top 3 must-see attractions in Paris?")
    print(result)

# Run the async main function - this creates and manages the event loop
if __name__ == "__main__":
    asyncio.run(main())

This pattern of wrapping your async code in a main() function and calling it with asyncio.run() is the standard approach for async programs. The asyncio.run() function handles all the event loop management automatically, making it the simplest way to execute async code.

Concurrent Execution with asyncio.gather()

The real power of async programming comes from running multiple operations concurrently. asyncio.gather() starts multiple coroutines simultaneously and waits for all of them to complete, returning results in the original order.

Here's how you can use it with Gemini API calls:

async def main():
    # Create coroutine objects (these don't run yet)
    tasks = [
        your_async_function("What are the top attractions in Paris?"),
        your_async_function("How do I get around Paris?"), 
        your_async_function("What are French cultural norms?")
    ]
    
    # Execute all tasks concurrently and wait for all to complete
    # Note: Results are returned in the same order as the original tasks
    results = await asyncio.gather(*tasks)
    print(results)

if __name__ == "__main__":
    asyncio.run(main())

The key insight: while one API call waits for Gemini's response, the event loop can initiate or continue processing other API calls. This transforms sequential waiting time into concurrent execution time.

Creating Async Functions for Gemini Calls

Now that you understand the fundamentals, let's build the foundation of our parallel workflow by creating an async function specifically designed for Gemini API calls. This function will handle individual questions while being optimized for concurrent execution.

async def ask_question(question):
    """
    Async function to ask Gemini a single question
    """
    print(f"🔄 Asking: {question}")
    loop = asyncio.get_running_loop()
    
    # Run the blocking Gemini call in a thread pool executor
    response = await loop.run_in_executor(
        None,
        lambda: client.models.generate_content(
            model=MODEL_NAME,
            contents=question,
            config=types.GenerateContentConfig(
                max_output_tokens=2000,
                temperature=0.7,
            ),
        ),
    )
    
    answer = response.text
    print(f"✅ Answered: {question}")
    return question, answer

The print statements help visualize when each question starts and completes, while returning a tuple of (question, answer) makes it easy to match responses back to their original questions when processing parallel results.

Preparing the List of Questions

With our async function ready, let's define the independent research questions that will form the parallel component of our workflow. Parallel processing shines when you have independent problems that don't rely on each other's answers:

# List of independent questions for Paris trip planning
questions = [
    "What are the top 3 must-see attractions in Paris?",
    "What is the most efficient way to get around Paris as a tourist?",
    "What are important cultural etiquette tips for visitors to France?"
]

These questions cover different aspects of travel planning (attractions, transportation, culture) and are completely independent of each other, making them perfect candidates for parallel execution.

Building Parallel Task Collections

Now let's put asyncio.gather() to work by creating multiple tasks that execute simultaneously. This is where the parallel magic happens:

async def main():
    """
    Execute parallel research and create comprehensive travel plan
    """
    print(f"Starting {len(questions)} research questions in parallel...")
    
    # Create tasks for all questions
    tasks = [ask_question(question) for question in questions]
    
    # Execute all tasks in parallel
    results = await asyncio.gather(*tasks)
    
    print("\nResearch completed!")

The list comprehension creates coroutine objects representing work to be done, while asyncio.gather(*tasks) starts all coroutines simultaneously and returns results in the original order regardless of completion sequence. Each result is a tuple containing the question and its corresponding answer.

Practical Safety Notes: Concurrency Limits and Partial Failures

Parallelism is powerful, but you should limit concurrency to avoid hitting rate limits or overwhelming the API. A simple pattern is to use a semaphore to cap in‑flight requests:

semaphore = asyncio.Semaphore(3)  # allow up to 3 concurrent requests

async def ask_question_limited(question):
    async with semaphore:
        return await ask_question(question)

You should also decide how to handle partial failures (some calls succeed while others fail). A common approach is to catch exceptions, log them, and continue aggregating successful results:

results = await asyncio.gather(*tasks, return_exceptions=True)
ok = [item for item in results if not isinstance(item, Exception)]

In production workflows, consider adding retries with backoff for transient errors and a clear fallback path when a subset of calls fails.

Aggregating Results for Final Analysis

With all our parallel research complete, let's build the aggregation phase that synthesizes everything into a comprehensive result. This sequential step ensures all information is properly integrated:

async def create_travel_plan(results):
    """
    Aggregate all research results into a comprehensive travel plan
    """
    # Combine all Q&A pairs into a formatted string
    combined_research = ""
    for question, answer in results:
        combined_research += f"{question}\n{answer}\n\n"
    
    # Create aggregator prompt
    aggregator_prompt = f"Create a brief Paris travel guide based on this research:\n\n{combined_research}\n\nMake it concise and practical."
    
    loop = asyncio.get_running_loop()
    response = await loop.run_in_executor(
        None,
        lambda: client.models.generate_content(
            model=MODEL_NAME,
            contents=aggregator_prompt,
            config=types.GenerateContentConfig(
                max_output_tokens=4000,
                temperature=0.7,
            ),
        ),
    )
    
    return response.text
Running the Complete Parallel Workflow

Let's bring it all together into a complete workflow that demonstrates the full power of parallel processing followed by intelligent aggregation:

import os
import asyncio
from google import genai
from google.genai import types

API = os.environ["GOOGLE_API_KEY"]
BASE = os.environ.get("GOOGLE_BASE_URL", "").rstrip("/")

client_kwargs = {"api_key": API}
if BASE:
    client_kwargs["http_options"] = types.HttpOptions(base_url=BASE)

client = genai.Client(**client_kwargs)

MODEL_NAME = "models/gemini-flash-latest"

questions = [
    "What are the top 3 must-see attractions in Paris?",
    "What is the most efficient way to get around Paris as a tourist?",
    "What are important cultural etiquette tips for visitors to France?"
]

async def ask_question(question):
    print(f"🔄 Asking: {question}")
    loop = asyncio.get_running_loop()
    response = await loop.run_in_executor(
        None,
        lambda: client.models.generate_content(
            model=MODEL_NAME,
            contents=question,
            config=types.GenerateContentConfig(
                max_output_tokens=2000,
                temperature=0.7,
            ),
        ),
    )
    answer = response.text
    print(f"✅ Answered: {question}")
    return question, answer

async def create_travel_plan(results):
    combined_research = ""
    for question, answer in results:
        combined_research += f"{question}\n{answer}\n\n"
    aggregator_prompt = f"Create a brief Paris travel guide based on this research:\n\n{combined_research}\n\nMake it concise and practical."
    loop = asyncio.get_running_loop()
    response = await loop.run_in_executor(
        None,
        lambda: client.models.generate_content(
            model=MODEL_NAME,
            contents=aggregator_prompt,
            config=types.GenerateContentConfig(
                max_output_tokens=4000,
                temperature=0.7,
            ),
        ),
    )
    return response.text

async def main():
    print(f"Starting {len(questions)} research questions in parallel...")
    tasks = [ask_question(question) for question in questions]
    results = await asyncio.gather(*tasks)
    print("\nResearch completed!")
    travel_plan = await create_travel_plan(results)
    print("\nParis Travel Guide:")
    print(travel_plan)

if __name__ == "__main__":
    asyncio.run(main())

When you run this workflow, you'll see the power of parallel execution unfold in three distinct stages:

  1. Instant Launch: All three "🔄 Asking" messages appear immediately as the API calls fire off simultaneously.
  2. Concurrent Completion: The "✅ Answered" messages arrive as Gemini finishes each response — often in a different order than they were asked, proving your requests are truly running in parallel.
  3. Intelligent Synthesis: All this concurrent research gets woven together into a comprehensive travel guide that combines the speed benefits of parallel processing with thoughtful analysis.

Example output:

Starting 3 research questions in parallel...
🔄 Asking: What are the top 3 must-see attractions in Paris?
🔄 Asking: What is the most efficient way to get around Paris as a tourist?
🔄 Asking: What are important cultural etiquette tips for visitors to France?
✅ Answered: What are the top 3 must-see attractions in Paris?
✅ Answered: What is the most efficient way to get around Paris as a tourist?
✅ Answered: What are important cultural etiquette tips for visitors to France?

Research completed!

Paris Travel Guide:
# Brief Paris Travel Guide

## Must-See Attractions
**Top 3 Essentials:**
1. **Eiffel Tower** - Best views at night when illuminated
2. **Louvre Museum** - Home to Mona Lisa; book tickets online
3. **Notre-Dame** - Gothic masterpiece (under restoration but exterior viewable)

*Tip: All three are within walking distance along the Seine - plan 1-2 days*

## Getting Around
**Best Option: Metro + Walking**
- €1.90 per ride or get a day pass
- Trains every 2-7 minutes
- Download Citymapper app
- Avoid rush hours (7:30-9:30am, 5:30-8pm)
- Most attractions within 500m of Metro stations

## Cultural Etiquette Essentials
**Must-Do's:**
- Say "Bonjour" entering shops, "Au revoir" leaving
- Dress nicely (no flip-flops/athletic wear)
- Keep hands visible while dining
- Learn basic French phrases - effort counts!

**Don't:**
- Smile at strangers (considered odd)
- Talk loudly in public
- Expect shops open 12-2pm (lunch break)

## Quick Tips
- Book Louvre tickets online to skip lines
- Bring shopping bags (stores don't provide them)
- Restaurants don't modify dishes - order as-is
- Evening Eiffel Tower visits are most magical

*Bon voyage!*
Performance Benefits and Use Cases

This two-stage approach provides significant performance benefits while maintaining result quality. The parallel research phase completes in roughly the time of the slowest individual question, while the aggregation phase ensures all information is properly synthesized into a usable travel plan.

This pattern works well for any scenario where you need to:

  • Research multiple independent topics quickly
  • Aggregate diverse information into a unified result
  • Balance speed with comprehensive analysis

The performance benefits are most significant when you have many independent research topics or when individual API calls have high latency.

Summary & Practice Preparation

You've mastered parallel processing patterns that transform slow sequential workflows into lightning-fast concurrent operations. The combination of parallel research gathering and sequential result synthesis provides both speed and quality, making it ideal for complex analysis tasks like travel planning, market research, or technical evaluations.

In the upcoming exercises, you'll apply these patterns to real-world scenarios and learn to handle the nuances of concurrent Gemini workflows. Remember: use parallel processing for independent research tasks, then aggregate results sequentially for comprehensive final analysis.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal