When I was adding AI image classification to my wallpaper app — one of several apps I've shipped since 2014 with over 50 million total downloads — I ran into a surprisingly tricky issue with the Gemini File API. After uploading an image, file.state would show PROCESSING, and no matter how long I waited, it never seemed to move forward.
My first instinct was to blame the network. The real cause turned out to be in my polling logic. After hitting this wall several times across different projects, I've settled on a pattern that handles it reliably.
What the PROCESSING State Actually Means
When you upload a file to the Gemini File API, it doesn't become immediately usable. Videos and large PDFs go through internal transcoding and analysis on Google's servers. During this time, the file stays in PROCESSING state.
Once processing completes, the state changes to ACTIVE, and only then can you include the file in a model request. If something goes wrong, the state becomes FAILED.
import google.generativeai as genai
genai.configure(api_key="YOUR_GEMINI_API_KEY")
# Upload a file
uploaded_file = genai.upload_file("sample_video.mp4", mime_type="video/mp4")
print(uploaded_file.state) # → FileState.PROCESSINGThe transition is always PROCESSING → ACTIVE or PROCESSING → FAILED. Sending a PROCESSING file to the model produces this error:
google.api_core.exceptions.FailedPrecondition: 400 File is not yet ready
Three Root Causes of Files Getting Stuck
What looks like "the file is stuck" actually has three distinct causes. The symptoms are similar enough that it's easy to misdiagnose.
Pattern 1: Polling runs only once
The most common mistake. The code checks file.state once, sees PROCESSING, and stops without a retry loop.
# ❌ Bad: checking state once and giving up
file = genai.get_file(uploaded_file.name)
if file.state == genai.protos.File.State.PROCESSING:
print("Still processing")
# Execution stops here — no loopYou need a loop that keeps checking until the state changes.
Pattern 2: Polling interval is too short
Polling every second with time.sleep(1) quickly hits the File API's rate limits. Suddenly you're mixing 429 errors into the output, making it much harder to tell what's actually happening with the file state.
Pattern 3: The file format itself isn't supported
If you pass an unsupported codec or specify the wrong MIME type, the file can sit in PROCESSING for a long time before eventually moving to FAILED. No amount of waiting will get it to ACTIVE.
The Right Polling Implementation: Exponential Backoff
Here's a production-ready polling function that combines exponential backoff with an overall timeout ceiling.
import google.generativeai as genai
import time
def wait_for_file_active(
file_name: str,
max_wait_seconds: int = 300, # 5 minute ceiling
initial_interval: float = 5.0, # start at 5-second intervals
max_interval: float = 30.0, # cap interval at 30 seconds
) -> genai.protos.File:
"""
Wait until a file reaches ACTIVE state.
Args:
file_name: The file's name field (e.g., "files/abc123")
max_wait_seconds: Raise TimeoutError if exceeded
initial_interval: First polling interval in seconds
max_interval: Maximum polling interval in seconds
Returns:
The File object in ACTIVE state
Raises:
TimeoutError: File didn't reach ACTIVE within max_wait_seconds
RuntimeError: File reached FAILED state
"""
start_time = time.time()
interval = initial_interval
attempt = 0
while True:
elapsed = time.time() - start_time
if elapsed > max_wait_seconds:
raise TimeoutError(
f"File {file_name} did not reach ACTIVE within {max_wait_seconds}s"
)
file = genai.get_file(file_name)
attempt += 1
print(f" [{attempt}] {file.name}: {file.state} ({elapsed:.0f}s elapsed)")
if file.state == genai.protos.File.State.ACTIVE:
print(f"✅ File is ACTIVE ({elapsed:.0f}s)")
return file
if file.state == genai.protos.File.State.FAILED:
raise RuntimeError(
f"File {file_name} processing failed: {getattr(file, 'error', 'unknown error')}"
)
# Exponential backoff: gradually increase the interval
time.sleep(interval)
interval = min(interval * 1.5, max_interval)Using it is straightforward:
# Upload
uploaded = genai.upload_file("document.pdf", mime_type="application/pdf")
print(f"Uploaded: {uploaded.name}")
try:
# Wait for ACTIVE
active_file = wait_for_file_active(uploaded.name, max_wait_seconds=180)
# Now use it with the model
model = genai.GenerativeModel("gemini-2.5-flash")
response = model.generate_content(["Summarize this document.", active_file])
print(response.text)
except TimeoutError as e:
print(f"Timed out: {e}")
genai.delete_file(uploaded.name) # Clean up
except RuntimeError as e:
print(f"Processing failed: {e}")Cleanup After Timeout or Failure
When a timeout or failure occurs, the uploaded file remains on Google's servers. File API retains files for 48 hours by default. For any workflow that processes large volumes of files, explicitly deleting after use is a good habit that prevents quota buildup.
import contextlib
@contextlib.contextmanager
def managed_file(file_path: str, mime_type: str):
"""Context manager that automatically deletes the uploaded file on exit."""
uploaded = genai.upload_file(file_path, mime_type=mime_type)
try:
active = wait_for_file_active(uploaded.name)
yield active
finally:
# Delete whether we succeeded or not
try:
genai.delete_file(uploaded.name)
print(f"🗑️ Deleted: {uploaded.name}")
except Exception:
pass # Don't let cleanup failures mask the original error
# Usage
with managed_file("video.mp4", "video/mp4") as file:
response = model.generate_content(["Describe this video.", file])
print(response.text)
# File is automatically deleted when the with block exitsMIME Type Mistakes That Cause Long FAILED Delays
Many cases where a file seems stuck in PROCESSING for an unusually long time end in FAILED, and the root cause is a wrong MIME type. For example, uploading an .mp4 file with mime_type="video/mpeg" will start processing but ultimately fail.
Common supported MIME types:
- Video:
video/mp4,video/mpeg,video/mov,video/avi,video/webm - Audio:
audio/mpeg,audio/wav,audio/ogg,audio/flac - Images:
image/jpeg,image/png,image/gif,image/webp - Documents:
application/pdf,text/plain,text/html
Using Python's mimetypes module to infer MIME type from the file extension removes a whole class of mistakes:
import mimetypes
def get_mime_type(file_path: str) -> str:
"""Infer MIME type from file path."""
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
raise ValueError(f"Cannot determine MIME type for: {file_path}")
return mime_type
# Usage
path = "sample_video.mp4"
mime = get_mime_type(path) # → "video/mp4"
uploaded = genai.upload_file(path, mime_type=mime)Parallel Uploads for Batch Processing
For the image classification feature in my wallpaper app, I needed to process many images at once. Uploading and waiting for each file sequentially was far too slow. Combining parallel uploads with async polling cuts processing time significantly.
import asyncio
import google.generativeai as genai
async def async_wait_for_active(file_name: str, max_wait: int = 300) -> genai.protos.File:
"""Async version of the file wait function."""
start = asyncio.get_event_loop().time()
interval = 5.0
while True:
elapsed = asyncio.get_event_loop().time() - start
if elapsed > max_wait:
raise TimeoutError(f"{file_name} timed out")
# genai.get_file is synchronous — run it in an executor
loop = asyncio.get_event_loop()
file = await loop.run_in_executor(None, genai.get_file, file_name)
if file.state == genai.protos.File.State.ACTIVE:
return file
if file.state == genai.protos.File.State.FAILED:
raise RuntimeError(f"{file_name} processing failed")
await asyncio.sleep(min(interval, 30.0))
interval *= 1.5
async def batch_upload_and_wait(file_paths: list[str]) -> list[genai.protos.File]:
"""Upload multiple files and wait for all to become ACTIVE in parallel."""
loop = asyncio.get_event_loop()
uploaded_files = []
for path in file_paths:
mime = get_mime_type(path)
f = await loop.run_in_executor(
None, lambda p=path, m=mime: genai.upload_file(p, mime_type=m)
)
uploaded_files.append(f)
print(f"Uploaded: {f.name}")
# Wait for all files in parallel
active_files = await asyncio.gather(
*[async_wait_for_active(f.name) for f in uploaded_files]
)
return list(active_files)
# Run example:
# files = asyncio.run(batch_upload_and_wait(["img1.jpg", "img2.jpg", "img3.jpg"]))API costs accumulate over time in long-running projects. Parallelizing uploads to reduce wall time, combined with prompt cleanup of finished files, is a pattern that pays off across many production use cases.
Troubleshooting Checklist
When you hit a PROCESSING wall, check these in order:
Do you have a polling loop, or does your code only check state once? Even working code usually fails without a retry loop. Is your polling interval at least 5 seconds? Shorter intervals risk triggering rate limits. Is the MIME type correct? Use mimetypes.guess_type() to be safe. Does your code have a max_wait_seconds ceiling? An uncapped loop is a production incident waiting to happen.
For a full walkthrough of File API upload options and supported file types, see the Gemini File API Guide. For broader error handling patterns across the Gemini API, Gemini API Error Handling Guide covers rate limits, 400 errors, and retry strategies.
Once the polling logic is properly in place, the File API becomes one of the more reliable tools in the Gemini ecosystem. Get the timeout design right once, and it runs quietly in the background.