GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
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-api277file-api6troubleshooting82python104error-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-06-12
Reverse-Engineering Empty Gemini API Responses with finish_reason — Triage, Retry Classification, and Monitoring
An empty response.text has three distinct failure layers — candidates, prompt_feedback, and finish_reason. Production code for detecting thinking-token starvation, classifying what is worth retrying, and monitoring your empty-response rate.
API / SDK2026-06-21
Gemini API Implicit Caching Not Working — Troubleshooting Guide by Root Cause
Troubleshoot Gemini API implicit caching issues: cache not hitting, unexpectedly high costs, or low cache hit rates. Covers token thresholds, prompt structure, model version consistency, TTL expiry, and multimodal caching with code examples.
API / SDK2026-06-14
When Gemini API Cuts Your Response Off Mid-Sentence — Detecting finish_reason: MAX_TOKENS and Stitching the Continuation
Long-form generation that ends mid-sentence is usually finish_reason: MAX_TOKENS. This failure arrives as a quiet HTTP 200, no exception. Here is how to detect it, stitch a continuation to recover the full text, and avoid the thinking-token trap that makes it worse on 3.x models.
📚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 →