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-19Advanced

Designing an Image Pipeline with Gemini Files API and Cloudflare R2 — Notes from Running a Wallpaper App

Notes from rebuilding the image processing pipeline of a wallpaper app around Gemini Files API and Cloudflare R2. Covers the 48-hour TTL, idempotent retries, and cost monitoring, with implementation code and 30 days of numbers.

Gemini API192Files API4Cloudflare R2indie developer12image processingwallpaper app

Premium Article

I'm Masaki Hirokawa, an indie developer and contemporary artist. Since 2013, I've been running wallpaper apps that have crossed 50 million downloads in total. Once the new-image intake reached a few hundred per day, the small inefficiencies in my pipeline started showing up at the end of the month.

The first version simply uploaded each image to the Gemini Files API on every job. Files there expire after 48 hours, and any naive retry would re-upload the same bytes again and again. I spent about a month rewriting the pipeline so that Cloudflare R2 owns the long-lived bytes and Gemini Files API only holds short-lived processing references. These are the notes from that rebuild.

It started the day I realised I was uploading the same image three times

In early March 2026, something looked off in my admin dashboard. About 400 new images were being added per day, but the Gemini Files API invoices were charging me for almost three times that many uploads.

The reason became obvious quickly. The Files API auto-deletes files after 48 hours, and my old pipeline retried on any error, including transient network blips and short-lived 5xx responses. The same bytes were being uploaded over and over.

The lesson I took from that week was simple. Files API is best treated as a per-session scratch space, not as persistent storage. Persistence belongs in another layer, and Cloudflare R2 turned out to fit that role nicely.

How I split responsibilities between Files API and R2

The shape I ended up with is a two-layer storage:

LayerRoleLifetimeCost feel
Cloudflare R2Permanent home for the image bytes, also serves themForeverFree egress, ~¥2/GB·month
Gemini Files APIProcessing-session reference for Gemini calls48 hoursPer-upload charge

I anchored the design around three principles:

  1. Every image is written to R2 first; the R2 object key is the canonical address
  2. Files API is only touched at the moment Gemini needs to read the image, and the reference is treated as disposable
  3. Retries cache the fileUri; we never re-upload to Files API if a valid reference still exists

The upload helper that came out of these rules looks like this in TypeScript on Cloudflare Workers:

// src/lib/image-pipeline/upload.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
import { createHash } from "node:crypto";
 
interface UploadResult {
  fileUri: string;       // Gemini Files API reference
  fileHash: string;      // SHA-256 of the bytes
  expiresAt: number;     // Unix milliseconds
  r2Key: string;         // canonical object key
}
 
export async function ensureGeminiFile(
  bytes: Uint8Array,
  mimeType: string,
  kv: KVNamespace,        // Cloudflare KV (metadata)
  bucket: R2Bucket,       // Cloudflare R2 (bytes)
  apiKey: string,
): Promise<UploadResult> {
  const fileHash = sha256(bytes);
  const cacheKey = `gemini-file:${fileHash}`;
  const r2Key = `originals/${fileHash}.bin`;
 
  // 1) Persist to R2 if missing (put is naturally idempotent)
  if (!(await bucket.head(r2Key))) {
    await bucket.put(r2Key, bytes, { httpMetadata: { contentType: mimeType } });
  }
 
  // 2) Try to reuse the Files API reference from KV
  const cached = await kv.get<UploadResult>(cacheKey, "json");
  if (cached && cached.expiresAt > Date.now() + 5 * 60_000) {
    return cached; // reuse if at least 5 minutes left
  }
 
  // 3) Upload to Files API only when we have to
  const genAI = new GoogleGenerativeAI(apiKey);
  const file = await genAI.files.upload({
    file: new Blob([bytes], { type: mimeType }),
    config: { displayName: fileHash },
  });
 
  const result: UploadResult = {
    fileUri: file.uri,
    fileHash,
    expiresAt: Date.now() + 47 * 60 * 60_000, // treat as expired one hour early
    r2Key,
  };
  await kv.put(cacheKey, JSON.stringify(result), { expirationTtl: 47 * 3600 });
  return result;
}
 
function sha256(b: Uint8Array): string {
  return createHash("sha256").update(b).digest("hex");
}

R2 writes are naturally idempotent because the key is the content hash. The Files API reference is cached in KV with a 47-hour TTL. The deliberate one-hour margin matters: if you cache for the full 48 hours and the reference is consumed at the very last minute, Gemini will tell you the file has already expired.

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 two-layer storage design that treats Files API as a 48-hour processing session and R2 as the source of truth
Hash-keyed idempotency that prevents the same bytes from being uploaded twice across retries
Rate-limited retries and a three-metric dashboard that brought monthly waste down to a few thousand yen
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-06-28
Read Video with Timestamps in the Gemini API: Pull Just the Scene You Need
Hunting for 'where was that step?' in a screen recording or app demo is a chore. Here is how to use Gemini API video understanding to pull just the right scene with timestamps, plus a design that keeps tokens down with FPS and resolution.
API / SDK2026-06-20
Catching Gemini Model Deprecations in CI Before They Bite
Build a small guard that scans your codebase for hardcoded Gemini model IDs, cross-checks shutdown deadlines, and turns CI red before a model quietly disappears.
API / SDK2026-06-03
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.
📚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 →