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.
When you're running apps with over 50 million cumulative downloads across the wallpaper and wellness categories, keeping metadata updated in 12 languages is just part of the job. But 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.
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.
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.
Parallelizing with asyncio.gather()
Python's asyncio combined with the Gemini SDK's async method (generate_content_async) lets you fire off multiple requests simultaneously. The key is using a Semaphore to cap concurrency and avoid rate limit errors.
import asyncio
import google.generativeai as genai
import time
genai.configure(api_key="YOUR_GEMINI_API_KEY")
LANGUAGES = [
"Japanese", "English", "Chinese (Simplified)", "Chinese (Traditional)",
"Korean", "French", "German", "Spanish", "Portuguese", "Italian",
"Arabic", "Russian"
]
async def generate_description_async(
model: genai.GenerativeModel,
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.
"""
response = await model.generate_content_async(prompt)
return lang, response.text
async def generate_all_languages(app_name: str) -> dict[str, str]:
model = genai.GenerativeModel("gemini-2.5-flash")
# Cap at 5 concurrent requests to stay within RPM limits
semaphore = asyncio.Semaphore(5)
tasks = [
generate_description_async(model, 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 secondsThe measured result was around 65 seconds — down from 780. That's roughly a 12x speedup.
Pitfall 1: 429 Rate Limit Errors Under Concurrency
The more you parallelize, the more you'll see ResourceExhausted (429) errors. 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.
During my own runs, I hit rate limits when I set the Semaphore too high. Here's the exponential backoff retry wrapper I settled on:
import asyncio
import random
import google.generativeai as genai
from google.api_core import exceptions as google_exceptions
async def generate_with_retry(
model: genai.GenerativeModel,
prompt: str,
semaphore: asyncio.Semaphore,
max_retries: int = 3
) -> str:
"""
Retry with jittered exponential backoff on rate limit errors.
"""
async with semaphore:
for attempt in range(max_retries):
try:
response = await model.generate_content_async(prompt)
return response.text
except google_exceptions.ResourceExhausted:
if attempt == max_retries - 1:
raise
wait = (2 ** attempt) + random.uniform(0, 1)
print(f"⚠️ Rate limit hit — retrying in {wait:.1f}s")
await asyncio.sleep(wait)For the free tier (15 RPM), keep your Semaphore at 3–4. On paid plans, 5 has been stable for me. Tuning this number is more important than it looks.
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] = textWith 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():
model = genai.GenerativeModel("gemini-2.5-flash")
semaphore = asyncio.Semaphore(5)
all_tasks = []
for app in APPS:
for lang in LANGUAGES:
all_tasks.append(
generate_description_async(model, app, lang, semaphore)
)
results = await asyncio.gather(*all_tasks, return_exceptions=True)
# Organize results into {app: {lang: text}} structureIn practice, 48 requests complete in 90–120 seconds. The sequential equivalent would have been over 50 minutes.
What This Means for Independent Developers
I started building apps independently in 2013 and have been maintaining them for over 12 years. One thing I've learned is that script performance directly affects motivation.
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 make it better. After parallelizing, I started checking each language's description individually and made small quality improvements I would have skipped before.
The asyncio approach isn't magic, but it removes friction. That reduction in friction compounds over time.
If you're already using the Gemini API synchronously, the migration path is straightforward: replace generate_content() with generate_content_async(), wrap your calls in an async def, add a Semaphore, and use asyncio.gather(). Most of the logic stays the same.
Start with a small batch — 3 or 4 tasks — confirm it works, then scale up.
Reference: The Gemini API Python SDK documentation includes async examples.