◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, 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 — The old Deep Research agent shut down on 10/23. If calls fail, 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-06-03Advanced

Reconciling Orphaned Gemini Files API Uploads Across a Fleet of Apps

Files API uploads quietly expire after 48 hours. Here's how I keep orphaned files and quota under control across six apps, using reconciliation against my own database and a scheduled cleanup job — written up as production notes from running wallpaper apps.

Gemini API251Files API4Production33Cost Optimization13Indie Development16

✦ Premium Article

One morning my wallpaper-classification batch was throwing File ... is not found over and over and had stalled. Images I was certain I had uploaded the night before had simply vanished from Gemini's Files API. The reason was immediate: files uploaded to the Files API expire automatically 48 hours after upload. I "knew" that, in the sense that I had read it, but I had never actually built that fact into my design.

I have been building apps on my own since 2014, mostly wallpaper and relaxation titles, and they have reached somewhere around 50 million downloads in total. Lately I let Gemini's multimodal capabilities handle the category classification of those wallpaper assets, uploading images for six apps in daily batches. The more volume I pushed through, the more this "expires in 48 hours" behavior — and the state management around it — became the pressure point of the whole operation. This article is not about the one-off question of "what do I do when a file disappears." It is about how to keep orphaned files and quota consumption under control across a fleet of apps, with the reconciliation and cleanup design written out in code.

Get the Files API lifecycle exactly right

Start with the facts that anchor the design. Run something to production on a fuzzy understanding of these and you will get hurt later.

Files uploaded to the Files API come with a few constraints you cannot negotiate. First, retention is 48 hours, and it cannot be extended. Second, there is a cap on total storage per project; exceed it and new uploads are rejected. Third, a freshly uploaded file sits in the PROCESSING state and cannot be used for inference until it becomes ACTIVE. For large files such as video, that processing wait can be long enough to matter.

In other words, the Files API is not persistent storage. It is a temporary hand-off area that is valid for 48 hours. Miss that, and you end up saving the uri of an uploaded file in your own database to reuse it, only to watch everything break two days later. That is exactly what I did first.

The code to check state looks like this. The part that waits for ACTIVE after upload is non-negotiable in production.

import time
from google import genai
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
def upload_and_wait(path: str, timeout_s: int = 120):
    """Upload and wait until ACTIVE. Using it while PROCESSING will fail."""
    f = client.files.upload(file=path)
    deadline = time.time() + timeout_s
    while f.state.name == "PROCESSING":
        if time.time() > deadline:
            # Don't swallow stuck files; let the caller decide
            raise TimeoutError(f"{f.name} stuck in PROCESSING")
        time.sleep(2)
        f = client.files.get(name=f.name)
    if f.state.name != "ACTIVE":
        raise RuntimeError(f"{f.name} ended in state {f.state.name}")
    return f

The important parts are not handing a PROCESSING file to inference, and setting a timeout so you never spin forever. Early on I waited with sleep alone, and a single file stuck in processing kept my whole batch frozen until morning.

Why "upload and forget" falls apart

A common pattern in solo development is the naive assumption that once you upload, the API will take care of the rest. It is insidious precisely because small experiments never surface the problem.

My first version looked roughly like this. Look at it as the cautionary example.

# Before: upload and never track state
def classify_wallpaper(path: str):
    f = client.files.upload(file=path)
    resp = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[f, "Classify this image into one of 30 categories"],
    )
    return resp.text
    # f is never referenced again. Never deleted. Never recorded.

This has three problems. First, the uploaded file is never deleted, so it wastes storage for a full 48 hours. Second, there is no record of which file was uploaded when, so you can never answer "how many files are currently sitting on the Files API side." Third, when classification fails partway through, the already-uploaded file is left dangling. That is an orphaned file.

At a few dozen a day, the damage looks small because expiry cleans up after 48 hours. But once you are uploading several thousand a day across six apps, the story changes. A design that only waits for expiry will pin against the storage cap and start rejecting new uploads, and more importantly, the state of "I don't know what's still out there" becomes a constant source of operational anxiety. My app revenue depends almost entirely on AdMob, so I am sensitive to cost down to the yen. Resources running in the background that I cannot account for felt wrong on their own.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A reconciliation script that detects orphaned files by comparing the Files API against your own database, built around the 48-hour expiry
✦How to account for quota and cost once uploads reach thousands per day, and why you should delete actively instead of waiting for expiry
✦Naming conventions and a cleanup job that let one person run six apps in parallel without losing track of state
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

◈ API / SDK2026-07-30
What Decided Our Cascade's Economics Wasn't the Escalation Rate
Putting Flash-Lite in front of 3.6 Flash and escalating only the hard items looks like an easy win. Estimating it from the escalation rate alone will mislead you. Here is the break-even solved with the escalated subset's output length in the equation.
◈ API / SDK2026-06-12
Building an App Store Rejection Workflow with the Gemini API — From Structured Notices to Resolution Center Replies
How I use the Gemini API to parse App Store rejection notices into structured JSON, cross-check guidelines, draft Resolution Center replies, and run pre-submission checks as an indie developer.
◈ API / SDK2026-05-21
Designing Event-Driven AI Workflows with Gemini API and Cloud Pub/Sub — Notes from an Indie Developer
An implementation memo on wiring Gemini API into Cloud Pub/Sub event-driven workflows. Using an app-review analysis pipeline as the running example, the article covers retry policy, dead-lettering, idempotency, and cost guardrails — from the perspective of someone running it solo.