◉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, 6 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, 6 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-24Intermediate

Why Your Gemini File API Uploads Vanish After 48 Hours — and How to Code Around It

Gemini File API resources are auto-deleted 48 hours after upload. Here is how to recognize the failure, why it happens, and concrete patterns for re-uploading, falling back to inline data, and managing expiration safely.

Gemini API249File APITroubleshooting8Python51Production33

One morning a classification script that I had been running over AdMob reports failed with an error I had never seen before. The code had not changed since the day before, yet the call that referenced an already-uploaded PDF came back with 403 PERMISSION_DENIED.

The reason turned out to be simple: every file sent through files.upload() is automatically deleted 48 hours after upload. As an indie developer who has been shipping apps since 2014 (Hirokawa, @dolice), I have learned that TTL behavior in external APIs is the kind of trap that bites long after the integration looks "done", and the Gemini File API is no exception.

This post walks through how to identify the failure, which TTL-safe patterns exist for passing files into a model, and how to build a small "assume re-upload" layer once you decide to operate around the limit.

Pin down which error you are actually seeing

A TTL expiration shows up in several different shapes depending on the SDK version and how you call the API. Identifying which variant you are looking at is the fastest way to a fix.

The three you are most likely to encounter:

  • PERMISSION_DENIED: returned by generateContent when you pass a file_uri that refers to a deleted resource. Misleading because it tempts you to suspect API key permissions
  • NOT_FOUND: returned by files.get(name=...) when you check the resource individually
  • INVALID_ARGUMENT: File ... is not in an active state: occasionally surfaces while the file is mid-deletion

The cleanest way to distinguish a real auth failure from a TTL miss is to ask the Files endpoint directly.

from google import genai
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
file_name = "files/abc123xyz"  # the name returned by a previous upload()
 
try:
    meta = client.files.get(name=file_name)
    print(meta.state, meta.expiration_time)
except Exception as e:
    print(f"file is gone or inaccessible: {e}")

expiration_time comes back as ISO 8601. If it is already in the past, you have a TTL miss, not an authentication problem.

Treat the 48-hour TTL as a hard contract

The File API is a short-term staging area, not a storage service.

  • Uploaded files are deleted automatically after 48 hours; there is no extension endpoint
  • Referencing a file_uri after deletion returns PERMISSION_DENIED
  • The per-file limit is 2 GB, and a single project can hold up to 20 GB
  • Calling files.upload() on the same bytes always produces a new name

Designing around this means accepting that "upload once, reuse forever" is not a supported pattern. For real persistence, the standard play is to keep the source in Cloud Storage and reach it through Vertex AI instead.

This is documented in the official Files API reference, but if you have been porting code from an older SDK or following half-stale snippets, the TTL is easy to miss.

Pattern 1: upload immediately before every call

The most straightforward fix is to call files.upload() right before each generateContent. It is wasteful in pure throughput terms, but for jobs that run a handful of times a day the simplicity is worth it.

from pathlib import Path
from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
def classify_pdf(pdf_path: Path, prompt: str) -> str:
    """Minimal upload → classify → delete cycle."""
    uploaded = client.files.upload(file=pdf_path)
    try:
        result = client.models.generate_content(
            model="gemini-2.5-flash",
            contents=[uploaded, prompt],
        )
        return result.text
    finally:
        # Delete immediately so we never bump into the 20 GB project ceiling
        client.files.delete(name=uploaded.name)

The finally block matters more than it looks. If you forget it, a long-running worker can quietly eat through the project quota in a couple of weeks. I have seen this happen on a shared key shared across multiple wallpaper apps on App Store and Google Play.

Pattern 2: fall back to inline data for small files

When the payload is small enough that the whole request stays under roughly 20 MB, you can skip the File API entirely and pass the bytes through inline_data.

import base64
from pathlib import Path
from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
def classify_small_pdf(pdf_path: Path, prompt: str) -> str:
    data = pdf_path.read_bytes()
 
    part = types.Part.from_bytes(
        data=data,
        mime_type="application/pdf",
    )
 
    result = client.models.generate_content(
        model="gemini-2.5-flash",
        contents=[part, prompt],
    )
    return result.text

Inline mode is TTL-free and easy to retry, but the request body grows fast. Past about 3 MB you will start to feel the added latency. A small router that picks the right path by file size keeps both worlds usable.

THRESHOLD_BYTES = 20 * 1024 * 1024  # 20 MB
 
def smart_classify(pdf_path: Path, prompt: str) -> str:
    if pdf_path.stat().st_size < THRESHOLD_BYTES:
        return classify_small_pdf(pdf_path, prompt)
    return classify_pdf(pdf_path, prompt)

Pattern 3: build a local expiration cache for reuse

If you genuinely need to reuse the same file across many calls within a day, the realistic approach is to maintain your own (local_path, remote_name, expires_at) table in Redis or SQLite and check validity before each use.

import datetime as dt
import sqlite3
from pathlib import Path
from google import genai
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
SAFETY_MARGIN = dt.timedelta(hours=2)  # refresh 2h before expiration
 
def get_or_refresh(conn: sqlite3.Connection, local_path: Path) -> str:
    row = conn.execute(
        "SELECT remote_name, expires_at FROM files WHERE local_path = ?",
        (str(local_path),),
    ).fetchone()
 
    now = dt.datetime.now(dt.timezone.utc)
 
    if row:
        remote_name, expires_at = row
        if dt.datetime.fromisoformat(expires_at) - SAFETY_MARGIN > now:
            return remote_name
        try:
            client.files.delete(name=remote_name)
        except Exception:
            pass
 
    uploaded = client.files.upload(file=local_path)
    conn.execute(
        "INSERT OR REPLACE INTO files VALUES (?, ?, ?)",
        (str(local_path), uploaded.name, uploaded.expiration_time.isoformat()),
    )
    conn.commit()
    return uploaded.name

The two-hour margin exists because referencing a file right at its expiration time occasionally surfaces a transient INVALID_ARGUMENT. If your batch runs at 2 a.m., it is worth confirming that no expirations land in that window.

Things to double-check before going to production

A few items that I always verify before turning a File API integration loose:

  1. expiration_time is in UTC, so never compare it directly to local time
  2. files.list() returns at most 100 entries per call, so you need to walk page_token for a full inventory
  3. The state field varies subtly between SDK versions (ACTIVE / PROCESSING / FAILED); string matching tends to survive upgrades better than enum comparison
  4. In CI, upload a small test PDF each run instead of sharing a long-lived record across jobs

For my own apps, a weekly App Store review classifier eventually settled on "do not trust the TTL — upload every time" as the safest default. Capacity and bandwidth are cheap enough today that the simplicity has been worth it.

Hope this saves a debugging session for anyone hitting the same wall. Thanks for reading.

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-08-17
After generated_images Disappeared: Three Branches Between the Response and a Saved File
Once you switch to generate_content, the line that breaks is usually the save. Here are the three branches on the response side - zero image parts, a shifting image count, and picking the extension - plus a receiver function you can drop in.
◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.