◉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-10Intermediate

Gemini API: RESOURCE_EXHAUSTED When Sending Large PDFs or Videos via inlineData — When to Switch to Files API

Why the Gemini API returns RESOURCE_EXHAUSTED for large PDFs or videos sent via inlineData, and a practical migration path to Files API based on real indie-developer experience.

gemini-api286files-api5troubleshooting87pdf6inline-data

A few days ago, an internal tool I had built on top of my own app business — the one that has accumulated over 50 million downloads since 2014 — started failing the moment a user uploaded a roughly 20 MB PDF. The same pipeline had been happily summarising smaller PDFs for weeks, so the sudden RESOURCE_EXHAUSTED was both confusing and quietly demoralising for the half hour I spent staring at logs.

The short version: a single Gemini API request has an overall payload ceiling of about 20 MB, and inlineData makes that ceiling tighter than it looks because Base64 encoding inflates the binary by roughly 4/3. For anything close to production, the real question is where in your pipeline you decide to switch over to the Files API. This article walks through that decision line and the actual migration steps.

Why inlineData Fails for Larger Files

Gemini API requests have an overall size cap of around 20 MB including metadata. When you embed a file directly via inlineData, the payload is Base64-encoded internally, which expands the original binary by about 33 percent.

Concretely:

  • 12 MB original → ~16 MB after Base64 → request fits, just barely
  • 16 MB original → ~21 MB after Base64 → exceeds the cap → RESOURCE_EXHAUSTED
  • 30 MB video → ~40 MB after Base64 → fails immediately

The error you actually see looks like this:

google.api_core.exceptions.ResourceExhausted: 429 RESOURCE_EXHAUSTED.
{'error': {'code': 429, 'message': 'Resource has been exhausted (e.g. check quota).', 'status': 'RESOURCE_EXHAUSTED'}}

The wording strongly suggests a quota issue, but payload-size violations come back with the very same status. That overlap is what makes this bug expensive to diagnose the first time you hit it. If your daily quota looks healthy in the Cloud console and yet a single request fails immediately, payload size is almost always the actual cause.

A Simple Rule for When to Switch

Google's documentation publishes the size limits but stops short of telling you where to draw the migration line. After more than ten years of shipping indie apps, the rule I now apply is straightforward:

  • If the total payload is likely to exceed 15 MB, start with the Files API
  • If the file will be passed to the API more than once, use the Files API
  • For one-off scripts and quick experiments, inlineData is fine
  • For any production system that accepts user uploads, default to Files API

The second point is easy to underestimate. With Files API you can reuse the same upload URI for 48 hours, which dramatically cuts both bandwidth and latency for any pipeline that runs the file through multiple prompts or models. In a recent audit of one of my pipelines, switching repeated calls from inlineData to a cached Files URI cut p95 latency by roughly 40 percent simply because the binary stopped being re-uploaded for every prompt iteration.

Migrating to Files API (Python)

Here is the "before" version using inlineData:

# inlineData approach — fine for small files
from google import genai
from google.genai import types
import pathlib
 
client = genai.Client(api_key="YOUR_API_KEY")
 
pdf_bytes = pathlib.Path("report.pdf").read_bytes()
 
response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=[
        types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf"),
        "Summarise the three most important points of this PDF in English.",
    ],
)
print(response.text)

This works fine until the PDF approaches 20 MB. Then the RESOURCE_EXHAUSTED errors start appearing intermittently. Here is the Files API equivalent:

# Files API approach — for >15 MB or reusable assets
from google import genai
import pathlib, time
 
client = genai.Client(api_key="YOUR_API_KEY")
 
# 1. Upload (processed in the background)
uploaded = client.files.upload(file="report.pdf")
 
# 2. Wait for processing to finish (videos and large PDFs may take a while)
while uploaded.state.name == "PROCESSING":
    time.sleep(2)
    uploaded = client.files.get(name=uploaded.name)
 
if uploaded.state.name == "FAILED":
    raise RuntimeError(f"Upload failed: {uploaded.state}")
 
# 3. Pass the URI directly into contents
response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=[
        uploaded,
        "Summarise the three most important points of this PDF in English.",
    ],
)
print(response.text)
 
# Expected output (truncated):
# 1. Market trends in 2026 around ...
# 2. Shifts in market share among the major players ...
# 3. Three growth axes anticipated next year ...

Two things are worth noting. First, videos and longer PDFs frequently come back with state == PROCESSING, so you must include the polling loop. Second, you can hand the upload result object straight into contents — no manual URI string construction required, unlike older SDK examples.

A common mistake is to skip the polling step and immediately pass the freshly uploaded object to generate_content. The call will not raise — it will simply return a low-quality summary because the model received an asset that was not yet ready. This silent degradation is much harder to spot than a clean error, which is why the polling loop deserves to live in a small helper function from day one.

TypeScript / Node.js Equivalent

The pattern is essentially identical with the JavaScript SDK. If your stack is Node.js or Cloudflare Workers, the migration looks like this:

// Files API in @google/genai (Node.js / Bun)
import { GoogleGenAI } from "@google/genai";
import * as fs from "node:fs";
 
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
 
async function summarise(pdfPath: string) {
  // Upload (returns immediately; processing continues server-side)
  let file = await ai.files.upload({
    file: pdfPath,
    config: { mimeType: "application/pdf" },
  });
 
  // Poll until ready
  while (file.state === "PROCESSING") {
    await new Promise((r) => setTimeout(r, 2000));
    file = await ai.files.get({ name: file.name! });
  }
  if (file.state === "FAILED") throw new Error("upload failed");
 
  const result = await ai.models.generateContent({
    model: "gemini-2.5-flash",
    contents: [
      { fileData: { fileUri: file.uri!, mimeType: file.mimeType! } },
      { text: "Summarise the three most important points in English." },
    ],
  });
  return result.text;
}

One subtle difference from the Python SDK is that the JavaScript SDK does not currently accept the file object directly inside contents; you have to construct a fileData part manually with fileUri and mimeType. Forgetting the mimeType field is the single most common source of silent failures here, so keep an eye on it during code review.

Side Effects to Watch For

Three pitfalls show up consistently after migration. I have run into all of them in production.

The first is the 48-hour expiry. Files uploaded via Files API are deleted automatically after 48 hours. Long batch jobs that pause between phases sometimes hit File URI not found halfway through. The safe pattern is to re-upload at the start of each batch session. The behaviour is covered in detail in Gemini API: Resolving "File URI Not Found" Errors After Restart.

The second is that the same filename produces different URIs each time you upload. upload() always creates a fresh resource, so cache the URIs in a dictionary keyed by content hash if you plan to re-process the same file repeatedly. SHA-256 of the bytes works well as a key and gives you cheap deduplication for free.

The third is upload failures themselves. On flaky networks the upload call can time out, so build in retry logic — or, for files larger than around 100 MB, switch to a Cloud Storage based pipeline. That route is documented in Gemini API: A 100 MB File Processing Pipeline via Cloud Storage.

How I Decide for My Own Projects

I have been building apps independently since 2014, and at the AdMob revenue peak that work brought in over 1.5 million yen per month. The lesson that experience hammered home is simple: cost and latency are decided in the first design pass, not later.

For a wallpaper app where users post images that an AI then categorises, files are typically under 1 MB and inlineData is the right choice. Conversely, when I analyse high-resolution files from my own art practice — RAW images that easily reach tens of megabytes — I upload through the Files API from the start so I can run multiple models against the same asset without paying the bandwidth cost twice.

A heuristic I use in code reviews of my own projects: any function that builds a Gemini request and accepts a user-supplied file should always go through the Files API. The marginal complexity of one extra upload step is much smaller than the operational cost of a payload-size outage on a Saturday evening when the pipeline starts trending on social media for unrelated reasons.

When you are unsure, ask yourself: "will this file be used in only this single request?" If yes, inlineData. If no or unclear, Files API. That single question prevents almost every quiet RESOURCE_EXHAUSTED outage.

For a deeper operational treatment of the Files API specifically, see Gemini API: Files API Troubleshooting Complete Guide.


One concrete next step: if you currently use inlineData anywhere, add a single log line that prints the payload size before each request. Once you can see how many of your daily calls cross the 15 MB threshold, the decision to migrate becomes obvious instead of theoretical. Hope this saves someone the same half hour of confused log staring.

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-20
Gemini API Streaming Works Locally but Buffers in Production — Fixing Cloud Run, Vercel, and Cloudflare
Streaming responses flow token-by-token in local dev, then arrive as one big blob in production. A walkthrough of the five most common causes — Cloud Run timeouts, Vercel runtime mismatch, Cloudflare Workers proxying, server-side text() pitfalls, and client-side decoding — with the fixes I use on my own sites.
◈ 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.