◉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-05-12Intermediate

Gemini File API Stuck in PROCESSING State: Timeout Handling and Retry Design

Fix Gemini File API files stuck in PROCESSING state. Learn proper polling with exponential backoff, timeout design, and cleanup strategies with working Python code examples.

gemini-api286file-api6troubleshooting89python108error-handling8

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.PROCESSING

The 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 loop

You 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 exits

MIME 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.

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 →

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-10-16
The Day /models Returns 200 but generateContent Returns 403 — Decide Your Triage Order Before It Happens
A triage order for the case where listing models succeeds with 200 but generation fails with 403 permission_denied. Includes a small diagnostic script that checks key, project, and billing in turn, why you should not retry a 403, and how to assemble what support needs.
◈ API / SDK2026-05-17
Diagnosing INVALID_ARGUMENT Errors in Gemini API Batch Image Analysis
When using the Gemini API to analyze multiple images at once, INVALID_ARGUMENT errors can be surprisingly hard to diagnose. This guide covers the three root causes—MIME type mismatches, inline data size limits, and contents structure errors—with code to fix each one.
◈ API / SDK2026-04-27
Why Your Gemini File URI Suddenly Returns 404 — Designing Around the 48-Hour TTL
Your Gemini-powered image or video pipeline worked perfectly yesterday, then started returning 404 the morning after a restart. The culprit is the File API's 48-hour TTL. Here is how to detect it and design an app that survives it.