Skip to content

Integrating Gemini AI Logic into Firebase Functions – Best Practices

Core Concept LearningAugust 3, 20269 min read

Calling the Gemini API from a Firebase Cloud Function looks like three lines of code — grab the API key, send a prompt, return the text — and that's exactly why it's easy to ship something that works in testing and leaks a key, exceeds its timeout, or accepts unauthenticated traffic in production. A callable function sitting between your client and a paid model API is a trust boundary, and treating it like a thin proxy skips the parts that actually matter: who's allowed to call it, where the API key lives, and what happens when the model call is the slow part of a request that also pays Cloud Functions' own cold-start tax.

This guide builds one function end to end: summarizeReview, an onCall Cloud Function that takes a product review and returns a one-sentence summary via Gemini. You'll wire up secret storage, add App Check so only your app can invoke it, trace one request through a cold start and a slow model call, and see what happens when the combined latency exceeds the function's timeout. For the client-side half of this pattern, see building a Gemini-powered chatbot.

Where a Gemini call sits inside a Firebase Function
Where a Gemini call sits inside a Firebase Function

Why the Gemini call belongs server-side

Putting a Gemini API key in a mobile or web client is not a minor security nit — it means anyone who decompiles the app or inspects network traffic has your API key and can run up your bill on someone else's account. A Cloud Function exists here for exactly one reason: it's a place to hold the secret and add authorization before the model ever sees a request, not just a convenient place to put backend code.

That reasoning also sets the shape of the function correctly: an onCall function that accepts a narrow, typed input ({ reviewText: string }), validates it, calls Gemini with a fixed prompt template, and returns a typed result. It is deliberately not a generic proxy that forwards arbitrary prompts from the client — a generic pass-through hands an attacker the same blank check a client-side key would, just routed through your infrastructure instead of directly to Google's.

Quick reference

  • Never bundle a Gemini API key in client code (mobile app, web bundle) — treat any client-embedded key as already leaked.
  • onCall functions get automatic Firebase Auth context and request/response marshalling — prefer them over raw onRequest HTTP functions for client-invoked AI calls.
  • Validate and bound the input shape server-side (max review length, required fields) before it reaches the model — an unbounded input is an unbounded token bill.
  • Do not build a generic "send any prompt to Gemini" endpoint for public clients — scope each function to one task with a fixed prompt template.

Remember this

The function's job is not just to "call Gemini from the backend" — it's to be the one place that holds the key, checks who's asking, and bounds what they can ask, none of which a client-embedded key can do.

Secret storage and App Check

Firebase Functions integrates with Google Cloud Secret Manager for exactly this case: the API key is stored outside your source tree, referenced by name in the function's configuration, and injected as an environment variable at runtime — never committed, never visible in the Firebase console's function source view. Loading it any other way (a hardcoded string, an .env file bundled into the deployed function source) reintroduces the same leak risk a client-side key has, just one hop removed.

App Check adds the other half: it verifies that a request actually comes from your registered app (via device attestation — Play Integrity, App Attest, or reCAPTCHA for web) rather than an arbitrary script that discovered your function's URL. Without it, an onCall function checks Firebase Auth for who the caller claims to be, but nothing verifies what is making the call — a script with a stolen or freely-created anonymous auth token can call it exactly like your real app can.

One review-summary request through the function
One review-summary request through the function

Quick reference

  • Reference secrets via defineSecret and --set-secrets, not process.env populated from a .env file checked into the repo.
  • enforceAppCheck: true rejects requests without a valid App Check token — enable debug tokens only in development, never leave them accepted in production.
  • Validate input shape and bounds before the Gemini call, not after — an oversized reviewText is a cost and latency problem you can reject for free.
  • Rotate the Secret Manager value periodically and audit which functions reference it — an unused reference to a rotated secret fails loudly at deploy time, which is the point.
Hardcoded key, no App Check
1import { onCall } from "firebase-functions/v2/https";2import { GoogleGenAI } from "@google/genai";3 4const ai = new GoogleGenAI({ apiKey: "AIzaSy...hardcoded" }); // leaks in source control5 6export const summarizeReview = onCall(async (request) => {7  const { reviewText } = request.data;8  const res = await ai.models.generateContent({9    model: "gemini-2.5-flash",10    contents: `Summarize this review in one sentence: ${reviewText}`,11  });12  return { summary: res.text };13  // No App Check enforcement — any client with a valid anonymous auth14  // token can call this and consume your Gemini quota.15});
Secret Manager + App Check enforcement
1import { onCall, HttpsError } from "firebase-functions/v2/https";2import { defineSecret } from "firebase-functions/params";3import { GoogleGenAI } from "@google/genai";4 5const geminiKey = defineSecret("GEMINI_API_KEY"); // backed by Secret Manager6 7export const summarizeReview = onCall(8  { secrets: [geminiKey], enforceAppCheck: true, timeoutSeconds: 30 },9  async (request) => {10    const reviewText = request.data?.reviewText;11    if (typeof reviewText !== "string" || reviewText.length > 2000) {12      throw new HttpsError("invalid-argument", "reviewText must be a string under 2000 chars");13    }14 15    const ai = new GoogleGenAI({ apiKey: geminiKey.value() });16    const res = await ai.models.generateContent({17      model: "gemini-2.5-flash",18      contents: `Summarize this review in one sentence: ${reviewText}`,19    });20    return { summary: res.text };21  }22);23// Break it: deploy with enforceAppCheck: false and call the function from24// curl with a forged anonymous token — expected (before the fix): it25// succeeds and burns your Gemini quota with no real client involved.

Remember this

Firebase Auth answers "who is this user," and App Check answers "is this request coming from my real app" — an onCall function calling a paid model API needs both, because either one alone still lets a scripted client through.

Cold starts, timeouts, and the latency budget

Cloud Functions has its own startup cost — a cold instance can add a second or two before your handler code even runs, on top of whatever the Gemini call itself takes. The default function timeout (60 seconds, configurable) sounds generous until you account for both costs stacking: a cold start plus a slow model response plus any retry logic can exceed it, and the client sees a generic DEADLINE_EXCEEDED with no useful signal about which part was slow.

Design the function's own internal timeout shorter than the platform timeout, and make it explicit — call Gemini with a bounded timeout (using an AbortSignal or the SDK's own request timeout option) well under the function's configured timeoutSeconds, so your code controls the failure and can return a clear, actionable error instead of the platform cutting the connection with no context.

Cold start plus a slow model call can exceed the function timeout
Cold start plus a slow model call can exceed the function timeout

Quick reference

  • Set timeoutSeconds explicitly on the function definition rather than relying on the platform default — pick a value that reflects your actual model-call SLA plus cold-start margin.
  • Bound the Gemini call itself with its own shorter timeout so your code, not the platform, decides what a slow call means and returns a specific error.
  • Consider minInstances: 1 for functions on the critical path if cold-start latency is unacceptable — it costs idle compute, so apply it selectively, not everywhere.
  • Log cold-start vs. warm-start latency separately in your observability stack — conflating them hides whether a slow response is a Gemini latency issue or a Functions platform issue.

Remember this

A function timeout failure with no further detail usually means two latencies stacked silently — cold start plus model latency — and the fix is giving the model call its own explicit, shorter timeout so the failure is diagnosable.

When Functions is the right layer

Firebase Functions is the right home for this pattern when the workload is request/response, bursty, and low enough in per-call latency budget to tolerate occasional cold starts — most single-model-call features (summarize, classify, extract) fit. Reach for a longer-running compute layer like Cloud Run background workers when the task is a multi-step agent loop, needs to hold state across a long-running job, or regularly exceeds a function's maximum timeout — retrofitting a long agent loop into a request/response function usually means fighting the platform's timeout instead of using it.

Measure before choosing: track p50/p95 latency for the Gemini call alone in a staging environment, add your realistic cold-start distribution, and compare the sum against your function's configured timeout with margin to spare. A design that only just clears the timeout on average will fail intermittently under any provider latency spike — plan for the tail, not the median.

Quick reference

  • Use Functions for short, single-call AI tasks; use a longer-running compute service for multi-step agent loops or anything holding state across a long job.
  • Measure p95 (not just average) latency for the Gemini call before setting your function's timeout — tail latency is what actually causes production timeouts.
  • Keep the client contract typed and narrow ({ reviewText } in, { summary } out) so a future model or prompt change doesn't require a client update.
  • Add a circuit breaker or fallback message for repeated Gemini failures rather than letting every retry burn its own cold-start-plus-call latency budget.

Remember this

Choosing Functions vs. a longer-running compute layer is a workload-shape decision — short single-model-call tasks fit request/response functions; multi-step agent loops need a layer that isn't fighting a timeout budget on every call.

Key takeaway

Deploy summarizeReview with a Secret Manager-backed API key, enforceAppCheck: true, input validation, and an explicit internal timeout shorter than the function's configured timeoutSeconds. Verify success by calling it from your real app with a short review and confirming a one-sentence summary returns within your latency budget.

Then break it deliberately: call the function's URL directly with curl and a forged or missing App Check token, and confirm it is rejected before reaching Gemini. Separately, force a slow response (a large reviewText near your validation limit) and confirm your code returns a clear timeout error rather than the platform's generic deadline-exceeded message. Pass criterion: unauthenticated calls are rejected pre-model, and slow calls fail with a diagnosable error your logs can actually explain.

Share:

Related Articles

A background worker that resizes uploaded images looks simple until traffic is spiky: quiet all morning, then two hundre

Read

As AI coding tools transition from individual developer utility to organization-wide engineering infrastructure, enterpr

Read

HTTPS and a valid JWT only prove the front door locked. Many real API incidents happen after authentication succeeds: a

Read

Keep learning

Follow a structured path or browse all courses to go deeper.