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-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-api277files-api5troubleshooting82pdf6inline-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-05-02
Gemini API PDF Input Troubleshooting: When Your Document Just Won't Read
When Gemini returns nothing for your PDF, hits the 20MB ceiling, or quietly skips pages, the symptom usually points to one of five very specific causes. Here's how to narrow it down quickly.
API / SDK2026-07-12
When a Whole Chapter Vanished From My Long-PDF Summary — Field Notes on Auditing Coverage
Chapter-wise summarize-then-merge works well on long PDFs, but a lost extraction or a merge step can drop an entire chapter while the pipeline finishes without a single error. This walks through catching those silent drops with one number—coverage—and re-running only the chapters that fell out.
API / SDK2026-07-01
When a Prompt That Worked in AI Studio Quietly Breaks Over the API — Field Notes on Measuring the Difference
A prompt that behaves perfectly in AI Studio returns an empty string or a 404 the moment you call the Gemini API from your own code. Instead of eyeballing the two, here is a small harness that records the config diff plus finish_reason, token usage, and the model name the server actually resolved — so you can isolate the cause by layer.
📚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 →