GEMINI LABJP
ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 termsROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms
Articles/API / SDK
API / SDK/2026-05-15Intermediate

Making Gemini API 12x Faster with asyncio: Lessons from Multilingual App Store Generation

How parallelizing Gemini API calls with asyncio cut multilingual App Store description generation from 13 minutes to 65 seconds — including Semaphore-based rate limit handling and what changed when moving from google-generativeai to google-genai.

gemini-api283asyncio3python104multilingual8indie-dev47app-store7python-sdk

In May 2026, I was simultaneously pushing major updates across four iOS apps — Beautiful HD Wallpapers, Ukiyo-e Wallpapers, Relaxing Healing, and Law of Attraction Everyday. The update involved new iPhone resolution support, AdMob mediation expansion, and StoreKit 2 migration. Amid all that, one task turned out to be an unexpected time sink: regenerating App Store descriptions across 12 languages.

Metadata for these apps lives in 12 languages. Every model swap or concept change means updating all of them, and the sequential script I had been using was painfully slow. Switching to asyncio with the Gemini API cut processing time to roughly one-twelfth of what it was. Here's how I did it.

One note before we start: this article has been rewritten since it was first published. The google-generativeai package I used at the time is now deprecated in favor of the unified google-genai SDK. The parallelization strategy hasn't changed at all, but the code has. I've kept both versions so you can see exactly what moved.

The Problem: 12 Languages × 4 Apps = 13 Minutes of Waiting

My original script used a simple for loop to generate descriptions one language at a time.

The snippet below is the original from May 2026, written against the legacy SDK. I wouldn't write it this way today, but it shows where the time actually went.

# ⚠️ Legacy SDK (google-generativeai — deprecated). Shown as historical context.
import google.generativeai as genai
import time
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-2.5-flash")
 
LANGUAGES = [
    "Japanese", "English", "Chinese (Simplified)", "Chinese (Traditional)",
    "Korean", "French", "German", "Spanish", "Portuguese", "Italian",
    "Arabic", "Russian"
]
 
def generate_description(app_name: str, lang: str) -> str:
    prompt = f"""
    Write an App Store description for "{app_name}" in {lang}.
    Keep it under 4000 characters. Focus on beauty, relaxation, and daily use.
    """
    response = model.generate_content(prompt)
    return response.text
 
# Sequential — slow
results = {}
start = time.time()
for lang in LANGUAGES:
    results[lang] = generate_description("Beautiful HD Wallpapers", lang)
    print(f"✓ {lang} done")
 
elapsed = time.time() - start
print(f"\nTotal: {elapsed:.1f}s")  # Measured: ~780 seconds (13 minutes)

About 780 seconds for 12 languages. Multiply that by 4 apps and you're looking at nearly an hour per update cycle — not a workflow I wanted to repeat.

Profiling the loop makes the waste obvious. Of the roughly 60 seconds each request took, my Python process was busy for a few dozen milliseconds. Everything else was time spent waiting on a socket.

Why asyncio Rather Than threading

threading is a reasonable alternative, but Gemini API calls are dominated by network I/O, not CPU work. For that shape of workload, asyncio is lighter and easier to reason about.

Threads each carry their own stack, and you end up hand-managing synchronization with locks or queues. With asyncio, a single Semaphore controls concurrency, and error handling stays in ordinary try/except form. For a solo project where maintenance cost matters more than raw throughput, that simplicity wins.

The SDK also supports async calls natively. The legacy SDK exposed generate_content_async(); the current google-genai SDK mirrors every method under client.aio. Either way, moving from synchronous code doesn't require restructuring your logic.

multiprocessing isn't needed here. It shines for CPU-bound work like image processing, but API calls are I/O-bound.

Parallelizing with asyncio.gather()

Here's the current-SDK version. The Semaphore caps concurrency, which doubles as rate limit protection.

# Current SDK: pip install -U google-genai
import asyncio
import time
from google import genai
 
# Reads GEMINI_API_KEY from the environment
client = genai.Client()
 
MODEL = "gemini-3.7-flash"
 
LANGUAGES = [
    "Japanese", "English", "Chinese (Simplified)", "Chinese (Traditional)",
    "Korean", "French", "German", "Spanish", "Portuguese", "Italian",
    "Arabic", "Russian"
]
 
async def generate_description_async(
    app_name: str,
    lang: str,
    semaphore: asyncio.Semaphore
) -> tuple[str, str]:
    """
    Generate a description with concurrency limited by semaphore.
    Returns (language, description) tuple.
    """
    async with semaphore:
        prompt = f"""
        Write an App Store description for "{app_name}" in {lang}.
        Keep it under 4000 characters. Focus on beauty, relaxation, and daily use.
        """
        # client.aio is the async entry point
        response = await client.aio.models.generate_content(
            model=MODEL,
            contents=prompt,
        )
        return lang, response.text
 
async def generate_all_languages(app_name: str) -> dict[str, str]:
    # Cap at 5 concurrent requests to stay within RPM limits
    semaphore = asyncio.Semaphore(5)
 
    tasks = [
        generate_description_async(app_name, lang, semaphore)
        for lang in LANGUAGES
    ]
 
    results = await asyncio.gather(*tasks, return_exceptions=True)
 
    output = {}
    for result in results:
        if isinstance(result, Exception):
            print(f"❌ Error: {result}")
        else:
            lang, text = result
            output[lang] = text
            print(f"✓ {lang} done")
    return output
 
start = time.time()
results = asyncio.run(generate_all_languages("Beautiful HD Wallpapers"))
elapsed = time.time() - start
print(f"\nTotal: {elapsed:.1f}s")  # Measured: ~65 seconds

The measured result was around 65 seconds — down from 780. That's roughly a 12x speedup.

Only three lines changed between SDK generations. Instead of passing a model object around, you hold one client, and await model.generate_content_async(prompt) became await client.aio.models.generate_content(model=..., contents=...). The Semaphore and gather() structure are untouched.

Pitfall 1: 429 Rate Limit Errors Under Concurrency

The more you parallelize, the more rate limit errors you'll see. The Gemini API free tier has a relatively low RPM cap, and even paid tiers have limits that matter when you're firing 48 requests in parallel. When I pushed the Semaphore to 10, several languages came back exhausted.

There's an important change here that trips people up when they follow older articles. The current google-genai SDK retries transient failures on its own — 429s and 503s included — starting at roughly a one-second delay, backing off exponentially up to 60 seconds, for up to four attempts by default. The hand-rolled backoff wrapper that used to be mandatory now stacks on top of the SDK's own retries if you write it the same way.

A wrapper is still worth having, but for a different job: catching what's left after the SDK gives up, so you can requeue that one language instead of losing it.

import asyncio
from google import genai
from google.genai import errors
 
client = genai.Client()
 
async def generate_with_fallback(
    prompt: str,
    lang: str,
    semaphore: asyncio.Semaphore,
) -> tuple[str, str | None]:
    """
    Return None only when the SDK's built-in retries are already exhausted,
    so the caller can requeue this language.
    """
    async with semaphore:
        try:
            response = await client.aio.models.generate_content(
                model="gemini-3.7-flash",
                contents=prompt,
            )
            return lang, response.text
        except errors.ClientError as e:
            # 429 RESOURCE_EXHAUSTED lands here
            if e.code == 429:
                print(f"⚠️ {lang}: rate limited — requeuing")
                return lang, None
            raise
        except errors.ServerError as e:
            print(f"⚠️ {lang}: server error ({e.code}) — requeuing")
            return lang, None

The exception types moved too. The legacy SDK raised google.api_core.exceptions.ResourceExhausted; the current SDK consolidates errors under google.genai.errors as ClientError (4xx) and ServerError (5xx), both subclasses of APIError. Since e.code carries the HTTP status, checking for 429 directly is the cleanest path.

The right Semaphore value depends on your plan. On the free tier, 3 was stable. On paid plans, 5 has been fine.

Pitfall 2: Error Handling with asyncio.gather()

By default, asyncio.gather() without return_exceptions=True will propagate the first exception and cancel remaining tasks. That means one failed language kills the whole batch.

# ❌ Dangerous: one error stops everything
results = await asyncio.gather(*tasks)
 
# ✅ Safe: errors are returned as values, not raised
results = await asyncio.gather(*tasks, return_exceptions=True)
 
for result in results:
    if isinstance(result, Exception):
        print(f"Skipping due to error: {result}")
    else:
        lang, text = result
        output[lang] = text

With 12 languages in flight, I'd rather get 11 successful results and log one failure than lose everything to a single timeout.

Running All 4 Apps in One Pass

The final version handles all 4 apps at once — 48 requests total.

APPS = [
    "Beautiful HD Wallpapers",
    "Ukiyo-e Wallpapers",
    "Relaxing Healing",
    "Law of Attraction Everyday"
]
 
async def generate_all_apps():
    semaphore = asyncio.Semaphore(5)  # 5 concurrent across ALL apps
 
    all_tasks = []
    for app in APPS:
        for lang in LANGUAGES:
            all_tasks.append(
                generate_description_async(app, lang, semaphore)
            )
 
    results = await asyncio.gather(*all_tasks, return_exceptions=True)
    # Organize results into {app: {lang: text}} structure

In practice, 48 requests complete in 90–120 seconds. The sequential equivalent would have been over 50 minutes.

The detail that matters: one Semaphore for the entire run, not one per app. Per-app semaphores would give you four groups of five, which is twenty concurrent requests walking straight into the rate limiter. Concurrency has to be budgeted against the total number of in-flight requests.

What the SDK Migration Actually Touched

Here's the diff, condensed. The whole migration took about half an hour.

AreaLegacy: google-generativeaiCurrent: google-genai
Installpip install google-generativeaipip install -U google-genai
Initgenai.configure(api_key=...)client = genai.Client() (reads GEMINI_API_KEY)
Model selectionBuild genai.GenerativeModel("...") up frontPass model= per call
Async callawait model.generate_content_async(prompt)await client.aio.models.generate_content(model=..., contents=...)
Generation optionsIndividual method argumentsConsolidated into config=types.GenerateContentConfig(...)
Exceptionsgoogle.api_core.exceptionsgoogle.genai.errors (ClientError / ServerError)
RetriesRoll your ownBuilt in (up to 4 attempts, exponential backoff)

The one that cost me real time was spelling. The legacy embedding call took content (singular); the current SDK standardizes on contents (plural). I stared at that single character for longer than I'd like to admit.

I also revisited the model name. The gemini-2.5-flash from the original script belongs to an older generation, so new code should target a current Flash model. Keeping the model ID in a single constant makes that a one-line change instead of a grep expedition. The full migration reference lives in Google's GenAI SDK migration guide.

Don't Skip Quality Review Just Because It's Fast

Speed has a side effect: it's tempting to ship output you haven't actually read. When I was generating one language at a time, odd phrasing was easy to catch. With everything arriving at once, it's easy to just scroll past.

Two languages needed close attention in my case. Arabic is right-to-left, so line wrapping in the App Store listing behaves differently from every other locale — grammatically correct output can still look wrong on the actual store page. Traditional Chinese sometimes reads like converted Simplified text, which feels slightly stiff for the Taiwan storefront.

Neither check gets faster through parallelization. But reviewing results after 65 seconds instead of 13 minutes leaves you with the patience to actually do it.

Saving generated output to disk matters too. Gemini responses vary between runs, so once you get a good one, version it. I store descriptions as app_descriptions/{app_name}/{lang}/v{date}.txt and diff against the previous version on each update.

Cutting the Wait Removes the Compromises

A script that takes 13 minutes becomes something you put off. A script that takes 65 seconds becomes something you run frequently — and when you run it frequently, you actually review the output and improve it.

After parallelizing, I started checking each language individually and made small quality fixes I would previously have waved through with "no time, ship it." The speedup itself matters less than the slack it creates.

If you're calling the Gemini API synchronously today, the migration path is short: switch to client.aio.models.generate_content(), wrap the call in an async def, add a Semaphore, and use asyncio.gather(). Most of the logic stays exactly where it is.


Start with Semaphore(3) and a three-language batch. Time one run before you scale up — having that number makes the effect of higher concurrency visible instead of theoretical. Current async examples live in the Gemini API library documentation.

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 $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-05-18
Gemini API asyncio Patterns for Production: How I Cut Processing Time by 80% in My Indie App Backend
A hands-on report on integrating Gemini API asyncio into a production backend. Covers Semaphore-based rate limiting, exponential backoff, and partial failure handling from real experience building a 50M+ download wallpaper app.
API / SDK2026-05-16
Automating App Store and Google Play Review Replies with Gemini API — The 8-Second Rule I Discovered the Hard Way
A practical implementation record of automating multilingual app review replies using Gemini API, based on managing apps with 50M+ cumulative downloads. Covers the undocumented 8-second wait rule that Apple's API silently enforces.
API / SDK2026-07-04
When Gemini API Leaks Japanese Into Your English Output Once in a While — Field Notes on Measuring the Contamination Rate and Tightening It in Stages
You told Gemini to answer in English, and 3 out of 100 runs slip a Japanese sentence into the tail. Here is why you cannot stop that 'once in a while', and a production pattern that measures the contamination rate as an SLO and tightens it with graded recovery, with working code.
📚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 →