先日、累計5,000万DLを超える自分のアプリ事業の延長で組んでいた「ユーザーがアップロードしたPDFをGeminiが要約する」内部ツールが、20MBほどのPDFを処理した瞬間に RESOURCE_EXHAUSTED で止まりました。テスト用の数MBのPDFでは順調に動いていたので、原因が分からずログを眺めていた数十分は地味につらかったです。
結論から書くと、inlineData で送れる容量にはリクエスト全体で20MBという壁があり、Base64 エンコードでさらに約4/3倍に膨らむため、見かけ15MB前後のファイルでも上限に当たります。本番運用では Files API に切り替える判断をどこで入れるかが鍵になります。本稿はその切り替えラインと、実際の移行手順を丁寧に追っていきます。
inlineData が大きいファイルで失敗する理由
Gemini API のリクエストには、メタデータも含めて1リクエスト合計約20MBの上限が設けられています。inlineData でファイルを直接埋め込む方式は、内部で Base64 にエンコードされるため、元のバイナリサイズに対して約 4/3 倍 に膨らみます。
具体的には次のようなイメージです。
- 元ファイル 12MB → Base64 後 約16MB → リクエスト全体 16MB+α → ギリギリ通る
- 元ファイル 16MB → Base64 後 約21MB → 上限超過 →
RESOURCE_EXHAUSTED - 元ファイル 30MB(動画) → Base64 後 約40MB → 即エラー
実際のエラーメッセージは次のような形で返ってきます。
google.api_core.exceptions.ResourceExhausted: 429 RESOURCE_EXHAUSTED.
{'error': {'code': 429, 'message': 'Resource has been exhausted (e.g. check quota).', 'status': 'RESOURCE_EXHAUSTED'}}
メッセージだけ見ると「クォータ超過」と勘違いしやすいのですが、ペイロードサイズが原因のときも同じステータスで返ってくるのが厄介な点です。
切り替え判断のシンプルなルール
公式ドキュメントには上限値の記載はありますが、「どこで Files API に切り替えるか」の判断基準までは踏み込んで書かれていません。10年以上個人で開発を続けてきた経験から、私は次のラインを目安にしています。
- ファイル合計が 15MB を超えそうなら最初から Files API
- 同じファイルを 2回以上 API に渡す予定があるなら Files API
- 検証用のスクリプトや単発処理は
inlineDataで十分 - ユーザーアップロードを受け付ける本番システムは Files API ベース固定
特に2点目は見落としやすいのですが、Files API を使えば一度アップロードしたファイルを48時間 URI で参照し続けられるため、再送信のコストとレイテンシを大幅に削減できます。
Files API への移行コード(Python)
実際に手を動かして比較します。まずは「動かなくなる前」の inlineData パターンです。
# inlineData 方式 — 小さいファイル向け
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"),
"このPDFの主要な論点を3つに絞って日本語で要約してください。",
],
)
print(response.text)PDFが10MB以下なら問題ありません。これが20MB近くになると、ある日突然 RESOURCE_EXHAUSTED が出始めます。次が Files API に切り替えた版です。
# Files API 方式 — 15MB超や再利用前提のファイル向け
from google import genai
import pathlib, time
client = genai.Client(api_key="YOUR_API_KEY")
# 1. アップロード(バックグラウンドで処理が走る)
uploaded = client.files.upload(file="report.pdf")
# 2. 処理完了を待つ(大きい動画やPDFは数秒〜数十秒)
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. URI を contents に渡して通常通り推論
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=[
uploaded,
"このPDFの主要な論点を3つに絞って日本語で要約してください。",
],
)
print(response.text)
# 期待する出力例:
# 1. 〇〇に関する2026年の市場動向 ...
# 2. 主要プレイヤー間のシェア変化 ...
# 3. 来年に向けた成長領域の3つの軸 ...ポイントは2つあります。動画やページ数の多いPDFは state が PROCESSING のまま返ってくることがあるため、必ず完了待ちのループを挟むこと。そして、アップロード結果オブジェクトをそのまま contents に渡せること(旧SDKのように URI 文字列を組み立てる必要はありません)。
TypeScript / Node.js での書き方
JavaScript SDK でも考え方は同じです。Node.js や Cloudflare Workers で運用している場合の最小構成を載せておきます。
// Files API @google/genai 版(Node.js / Bun)
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
async function summarise(pdfPath: string) {
// アップロード(すぐ返る。処理はサーバー側で継続)
let file = await ai.files.upload({
file: pdfPath,
config: { mimeType: "application/pdf" },
});
// 完了待ち
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: "このPDFの主要な論点を3つに絞って日本語で要約してください。" },
],
});
return result.text;
}Python SDK と異なるのは、JavaScript 側は contents にファイルオブジェクトを直接渡せず、fileData パートを手で組み立てる必要がある点です。mimeType の指定漏れは静かに失敗する典型例なので、レビュー時にここだけは必ず確認するようにしています。
ありがちな副作用と回避策
切り替え後にハマりやすい落とし穴を3つ挙げます。これも実際にアプリ運用中にぶつかったものです。
ひとつ目は 48時間の有効期限 です。Files API にアップロードしたファイルは48時間で自動削除されます。バッチ処理を分割して走らせると、後半で File URI not found が出ることがあるので、長時間ジョブでは「ジョブ開始時に再アップロード」を原則にしておくと安全です。詳しい挙動はGemini API: ファイルURIが再起動後に見つからないエラーの解消法で扱っています。
ふたつ目は 同名ファイルでも内容が違えば別URI という点です。upload() は呼び出すたびに新しいリソースを作るため、同じファイルを何度も処理する場合は URI を辞書に保持して使い回すのが定石です。
3つ目は アップロード自体の失敗 です。ネットワークが不安定な環境ではタイムアウトすることがあるので、リトライを実装するか、後述の Cloud Storage 経由ルートに切り替える判断も必要になります。100MBを超えるような大型ファイルはCloud Storage 経由の100MBファイル処理パイプラインが現実的です。
個人開発での使い分け(実践メモ)
私は2014年から個人でアプリ開発を続けていて、AdMob 収益のピーク時には月150万円を超える規模で運用していました。その経験から学んだのは、「コストとレイテンシは設計初期に決まる」という単純な事実です。
たとえば壁紙アプリでユーザーが投稿した画像をAIが分類するパイプラインなら、画像は基本的に1MB以下なので inlineData で十分です。一方、アート活動として手がけている高解像度の作品ファイル(数十MBのRAW画像など)を解析する際は、最初から Files API でアップロードして、複数モデルに横断的に推論させるほうがコストも時間も合理的です。
判断に迷ったら、「そのファイルをこのリクエスト1回しか使わないか?」を自分に問うのが一番速いです。Yesなら inlineData、Noや不明なら Files API。これだけで、今回のような RESOURCE_EXHAUSTED の地味な事故はほぼ防げます。
ファイル処理のさらに詳しい運用パターンはGemini API: Files API トラブルシューティング完全ガイドに整理しています。
最後に1つだけ次のアクションを示します。今すでに inlineData でファイルを送っているコードがあるなら、ファイルサイズを取得してログに出すコードを1行入れてみてください。15MBを超えるリクエストが1日に何件出ているか可視化されると、移行判断が一気に楽になります。同じ詰まりに遭遇している方の助けになれば嬉しいです。