The 60 Firebase questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Firebase is a backend-as-a-service platform from Google that gives client apps a hosted backend without you running servers. It bundles two NoSQL databases (Cloud Firestore and the older Realtime Database), an authentication service, file storage, static hosting, and Cloud Functions for server-side logic, all reached through client SDKs for web, iOS, and Android. Google acquired Firebase in 2014 and folded it into Google Cloud. Its signature trait is real-time sync: data changes push to every connected client instantly, and the SDKs cache offline so apps keep working without a network. In interviews, Firebase questions probe NoSQL data modeling, security rules (the code that guards your data), and the judgment of when a serverless backend fits versus when you'd run your own. This page collects the 60 questions that come up most, each with a direct answer and runnable code. The official Firebase documentation from Google is the canonical reference; increasingly the first backend round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Firebase in 100 Seconds
Video: Firebase in 100 Seconds (Fireship, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Firebase certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Firebase is Google's backend-as-a-service platform. It gives your app a hosted backend, databases, authentication, file storage, hosting, and serverless functions, all reached through client SDKs, without you provisioning, scaling, or running any servers yourself.
It solves the plumbing problem: instead of building auth, a real-time database, file uploads, and an API from scratch, you wire client SDKs to managed services and ship features. That's why it's popular for mobile apps, prototypes, and small teams that need a backend fast.
Key point: A one-line definition plus two concrete services (auth, Firestore) beats reciting the whole product list. Interviewers open with this to hear how you organize an answer.
The core set: Cloud Firestore and the Realtime Database (data), Authentication (sign-in), Cloud Storage (files), Hosting (static and dynamic web content), and Cloud Functions (server-side code). Around those sit Cloud Messaging (push notifications), Remote Config, App Check, and analytics via Google Analytics.
You don't need to list every product. Naming the data, auth, and functions trio and knowing what each does is the bar for a fresher round.
| Product | What it does |
|---|---|
| Cloud Firestore | NoSQL document database with real-time sync |
| Authentication | Email, phone, and social sign-in with managed sessions |
| Cloud Storage | Store and serve user files like images and video |
| Hosting | Serve static sites and single-page apps over a CDN |
| Cloud Functions | Run backend code on events or HTTPS requests |
Both sync data in real time and work offline, but they store data differently. The Realtime Database is one big JSON tree. Firestore stores documents grouped into collections, with richer queries, better scaling, and more granular security rules.
Firestore is the recommended default for new apps. The Realtime Database still fits simple, very low-latency state like presence or a live counter, where its flat The technical sequence is an advantage.
| Firestore | Realtime Database | |
|---|---|---|
| Data model | Documents and collections | One JSON tree |
| Querying | Rich, compound, sorted | Limited, mostly by key |
| Scaling | Automatic, large datasets | Best for smaller, flat data |
| Pricing driver | Per document read/write | Per GB downloaded and stored |
Key point: The follow-up is 'when would you still pick the Realtime Database?'. Have the presence or live-counter example ready.
A document is the unit of storage: a map of field/value pairs identified by a string ID, capped at 1 MB. A collection is a named container of documents. Documents can hold subcollections, which is how Firestore nests data.
So a path alternates collection then document: users/alice/orders/order123. Understanding this ladder is the foundation for every query and security rule.
import { doc, setDoc } from "firebase/firestore";
// collection "users" -> document "alice"
await setDoc(doc(db, "users", "alice"), {
name: "Alice",
role: "engineer",
createdAt: Date.now(),
});Firebase Authentication handles user sign-in and identity: it verifies credentials, issues tokens, and manages sessions so you don't store passwords yourself. Each signed-in user gets a stable uid you key your data on.
It supports email/password, phone (SMS), and federated providers like Google, Apple, Facebook, and GitHub, plus anonymous sign-in for guest sessions you can later upgrade to a real account.
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
const auth = getAuth();
const cred = await signInWithEmailAndPassword(auth, email, password);
console.log(cred.user.uid); // stable id you store data underWatch a deeper explanation
Video: Firebase - Ultimate Beginner's Guide (Fireship, YouTube)
Create a project in the Firebase console, register a web app, and copy its config object (API key, project ID, and so on). Install the SDK, then call initializeApp with that config once, and get handles to the services you need.
The API key here isn't a secret in the usual sense: it identifies your project, and security rules, not the key, control access. That surprises people, so it's a common follow-up.
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const app = initializeApp({
apiKey: "AIza...",
projectId: "my-app",
appId: "1:123:web:abc",
});
const db = getFirestore(app);Build a document reference with doc(db, collection, id), then call getDoc to fetch it once. The result is a snapshot object, not the data directly, so you check exists() before calling data(), because the document may not be there and reading a missing one shouldn't crash.
getDoc reads once. For live updates you'd use onSnapshot instead, which is a distinction interviewers like to draw out.
import { doc, getDoc } from "firebase/firestore";
const snap = await getDoc(doc(db, "users", "alice"));
if (snap.exists()) {
console.log(snap.data());
} else {
console.log("no such user");
}setDoc writes a document at an ID you choose, overwriting it (use merge: true to patch instead). updateDoc changes specific fields and fails if the document doesn't exist. addDoc creates a document with an auto-generated ID.
The choice signals intent: setDoc when you own the ID, addDoc when you want Firestore to assign one, updateDoc when you're editing an existing record.
import { doc, setDoc, updateDoc, addDoc, collection } from "firebase/firestore";
await setDoc(doc(db, "users", "alice"), { name: "Alice" });
await updateDoc(doc(db, "users", "alice"), { role: "lead" }); // patch
await addDoc(collection(db, "logs"), { at: Date.now() }); // auto idBuild a query with query(), add where() clauses for filters and orderBy() for sorting, then run getDocs and loop the snapshot. Firestore returns only documents that match, not the whole collection.
Firestore queries are shallow by default: they don't automatically pull subcollections, and every query must be backed by an index, which Firestore mostly creates for you.
import { collection, query, where, orderBy, getDocs } from "firebase/firestore";
const q = query(
collection(db, "orders"),
where("status", "==", "paid"),
orderBy("createdAt", "desc")
);
const snap = await getDocs(q);
snap.forEach((d) => console.log(d.id, d.data()));onSnapshot attaches a listener to a document or query. The callback fires once with current data, then again every time matching data changes on the server. This is what makes Firebase feel live: no polling, updates push to the client.
onSnapshot returns an unsubscribe function. Call it when the view unmounts, otherwise listeners leak and keep billing you for reads.
import { collection, onSnapshot } from "firebase/firestore";
const unsubscribe = onSnapshot(collection(db, "messages"), (snap) => {
snap.forEach((d) => render(d.data()));
});
// later, when leaving the screen
unsubscribe();Key point: Forgetting to unsubscribe is a real bug and a favorite follow-up. Mention it before they ask.
Watch a deeper explanation
Video: Getting Started with Firebase 9 #1 - Intro and What's New (Net Ninja, YouTube)
NoSQL means it doesn't use tables, rows, and SQL. Firestore stores schema-less documents, so two documents in the same collection can have different fields, and there are no joins across collections.
The practical consequence: you model data around how you'll read it, often duplicating data (denormalizing) so a screen can load from one query instead of joining several. That's the mental shift SQL developers have to make.
Firebase Hosting serves static and single-page web content (HTML, CSS, JavaScript, images) over a global CDN with automatic HTTPS. You deploy with the CLI, and it gives you fast, cached delivery plus atomic rollbacks.
It also integrates with Cloud Functions and Cloud Run so a request can hit dynamic backend code, which lets you host a full web app, front end plus API, on Firebase.
firebase init hosting # pick your public directory
firebase deploy --only hostingCloud Storage holds files: images, video, audio, PDFs, anything binary. Firestore holds structured data: fields and values. You upload a file to Storage, get back a download URL, and typically save that URL in a Firestore document.
The common pattern is both together: the photo bytes live in Storage, the caption and owner uid live in Firestore. Storage has its own security rules that mirror Firestore's rule syntax.
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const fileRef = ref(storage, `avatars/${uid}.jpg`);
await uploadBytes(fileRef, file);
const url = await getDownloadURL(fileRef); // save url in FirestoreAnonymous auth creates a temporary account with a real uid but no credentials, so a guest can use the app and own data immediately. Later you can link an email or Google account to that same uid, keeping their data.
It's useful for letting people try an app before signing up, or for carts and drafts that shouldn't require an account yet. Security rules can still gate access by uid because an anonymous user still has one.
Strings, numbers, booleans, null, timestamps, geopoints, byte blobs, arrays, and nested maps (objects). It also has references to other documents. There's no relational foreign key; a reference is just a typed pointer you resolve yourself.
The type worth calling out is the server timestamp: use serverTimestamp() so the write time comes from Google's clock, not the client's, which may be wrong or manipulated.
import { doc, setDoc, serverTimestamp } from "firebase/firestore";
await setDoc(doc(db, "events", id), {
title: "Launch",
attendees: ["alice", "bob"], // array
meta: { room: "A1", seats: 20 }, // nested map
createdAt: serverTimestamp(), // trusted server time
});Delete a whole document with deleteDoc on its reference. To remove a single field while keeping the rest of the document, use updateDoc and set that field to deleteField(). The two operations are different: one drops the record, the other just prunes one property from it.
Deleting a document does not delete its subcollections. Those are orphaned and still cost storage, which is a classic gotcha; you clean them up explicitly or with a Cloud Function.
import { doc, deleteDoc, updateDoc, deleteField } from "firebase/firestore";
await deleteDoc(doc(db, "users", "alice")); // whole document
await updateDoc(doc(db, "users", "bob"), {
tempToken: deleteField(), // one field
});Key point: Knowing that deleting a document leaves its subcollections behind separates people who've shipped from people who've only read docs.
The console is the web dashboard where you create projects, browse and edit Firestore data, manage users, write and test security rules, view analytics, and configure every product. It's where you go to inspect state and debug during development.
In an interview you'd mention that production changes belong in code and the CLI, not hand edits in the console, so environments stay reproducible.
Yes. The Spark plan is free with generous daily limits. Beyond that, the Blaze plan is pay-as-you-go. The thing to understand is what you pay for: Firestore bills per document read, write, and delete, plus storage and bandwidth, not per hour of uptime.
That per-operation model means a badly shaped query that reads thousands of documents to show ten can quietly run up a bill. Cost-aware querying is a real interview topic, not a footnote.
getDocs (and getDoc) reads once and resolves a promise with a point-in-time snapshot. onSnapshot attaches a persistent listener that fires immediately with current data and then again on every change, and it returns an unsubscribe function instead of a promise.
So it comes down to intent: a one-off fetch for a report or a form uses getDocs, while a live view that must update as data changes uses onSnapshot. Reaching for a listener when a single read would do wastes reads and complicates cleanup.
import { collection, getDocs, onSnapshot } from "firebase/firestore";
// one-time read
const snap = await getDocs(collection(db, "tasks"));
// live read, must unsubscribe later
const unsub = onSnapshot(collection(db, "tasks"), (s) =>
s.forEach((d) => render(d.data()))
);The Firebase CLI is the command-line tool for working with a project outside the console. You use it to deploy hosting, functions, security rules, and indexes, to run the local emulators, and to script setup so environments are reproducible instead of hand-configured.
In practice, rules and indexes live in files (firestore.rules, firestore.indexes.json) committed to your repo and pushed with firebase deploy, which is how a team keeps configuration in version control rather than clicking through the console.
npm install -g firebase-tools
firebase login
firebase deploy --only firestore:rules,functionsSecurity rules are code that runs on Google's servers to decide whether each read or write is allowed. They sit between client SDKs and your data, checking the request's auth state and the data itself against conditions you write, and no client can bypass them.
They matter because client code is public and can be edited by anyone. Rules are the real access-control layer: the client SDK only requests, and the rules on Google's servers decide what actually happens.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid} {
// a user can read/write only their own doc
allow read, write: if request.auth != null
&& request.auth.uid == uid;
}
}
}For candidates with working experience: data modeling, security rules, functions, and the judgment that separates users from understanders.
You model around your read patterns, not around normalization. Because there are no joins, you often duplicate data so a screen loads from one query. A common The technical sequence is top-level collections for entities plus subcollections for owned children, like users/{uid}/orders.
The trade-off is write complexity for read speed: denormalized data means updating the same fact in several places, sometimes via a Cloud Function. Getting this trade-off right is the core skill Firestore interviews test.
Key point: Say 'model for reads, denormalize deliberately, keep writes consistent with functions.' That sentence indicates real experience.
Use a subcollection when the child set is large or grows unbounded (a user's messages), because subcollection documents are queried and paginated independently and don't bloat the parent. Use a nested map or array for small, bounded, always-loaded-together data (an address, a few tags).
The reason is the 1 MB document limit and read cost: every read of a document pulls the whole thing, so stuffing a growing list into one document is both a size and a cost problem.
| Shape | Good for | Avoid when |
|---|---|---|
| Nested map | Small fixed structure (address) | It grows without bound |
| Array | Short lists, tags, member ids | Thousands of items or heavy querying |
| Subcollection | Large or growing child sets | Data is tiny and always read together |
Rules match paths and allow operations under conditions. You gate on request.auth for identity, on resource.data for the existing document, and on request.resource.data for the incoming write. You can read other documents with get() and exists() to enforce relationships, like checking a role document.
Test them with the Rules Playground in the console and the emulator's rules-unit-testing library in CI, so a rule change can't silently open your database.
match /posts/{postId} {
allow read: if true; // public read
allow create: if request.auth != null
&& request.resource.data.author == request.auth.uid;
allow update, delete: if request.auth != null
&& resource.data.author == request.auth.uid; // only the owner
}Key point: Naming request.resource.data (incoming) vs resource.data (existing) correctly is the tell that you've actually written rules.
Watch a deeper explanation
Video: Firestore Security Rules: How to Hack a Firebase App (Fireship, YouTube)
Cloud Functions run your backend code in a managed environment, triggered by events (a Firestore write, a new user, a file upload) or by HTTPS requests. You use them for logic that can't or shouldn't live on the client: sending emails, calling paid APIs with secret keys, aggregating data, or enforcing invariants.
The rule of thumb: anything requiring a secret, trust, or coordination across documents belongs in a function, because the client is public and untrusted.
import { onDocumentCreated } from "firebase-functions/v2/firestore";
export const onNewOrder = onDocumentCreated("orders/{id}", (event) => {
const order = event.data.data();
return sendReceiptEmail(order); // secret keys stay server-side
});A transaction reads and writes a set of documents atomically: either every operation commits or none does. Firestore transactions are optimistic. They read, compute, and if any read document changed before commit, Firestore retries the whole function.
You need one when a write depends on the current value of data that others might change at the same time: decrementing inventory, transferring a balance, claiming a unique slot. Without it, two clients can both read the old value and produce a wrong result.
import { runTransaction, doc } from "firebase/firestore";
await runTransaction(db, async (tx) => {
const ref = doc(db, "items", id);
const snap = await tx.get(ref);
const left = snap.data().stock;
if (left < 1) throw new Error("sold out");
tx.update(ref, { stock: left - 1 }); // atomic decrement
});A batched write groups up to 500 write operations that commit atomically as a unit, with no reads. A transaction reads then writes and can retry. Use a batch when you already know the values and just need several writes to succeed or fail together.
So the split is simple: need to read current data before deciding? Transaction. Just applying a known set of writes atomically? Batch. Batches are also cheaper because they skip the read-and-retry cycle.
import { writeBatch, doc } from "firebase/firestore";
const batch = writeBatch(db);
batch.set(doc(db, "users", uid), { active: true });
batch.update(doc(db, "stats", "global"), { users: increment(1) });
await batch.commit(); // both or neitherFirestore auto-creates single-field indexes, so a query filtering or ordering on one field just works. A query that combines fields, like where('status','==','open') plus orderBy('createdAt'), needs a composite index you define.
When you run an unindexed compound query, Firestore throws an error with a direct link to create the exact index. In production you commit those index definitions to firestore.indexes.json so they deploy with your code.
| Query shape | Index needed |
|---|---|
| where on one field | Automatic single-field index |
| where + orderBy on different fields | Composite index |
| multiple range filters on one field | Composite index |
| array-contains + another filter | Composite index |
Key point: Mentioning that the error message hands you the index link, and that you commit indexes to config, indicates production experience.
Use cursor-based pagination with orderBy plus startAfter(lastDocSnapshot) and limit(). You keep the last document snapshot from one page and pass it to startAfter for the next. Firestore has no offset(), because offsets would still read and bill for the skipped documents.
Cursor pagination is efficient: each page reads only its own documents. The catch is you must page forward through snapshots; jumping to an arbitrary page number isn't a native operation.
import { collection, query, orderBy, startAfter, limit, getDocs } from "firebase/firestore";
let last = null;
let q = query(collection(db, "posts"), orderBy("createdAt"), limit(20));
if (last) q = query(q, startAfter(last));
const snap = await getDocs(q);
last = snap.docs[snap.docs.length - 1]; // cursor for next pageThe SDKs cache data locally. Reads can be served from the cache when the network is down, and writes are queued locally, applied to the UI immediately (optimistic), then synced to the server when connectivity returns. Listeners keep firing from the cache so the app stays live offline.
The subtle part is conflict handling: last write wins by default on the server, and serverTimestamp() resolves to the real time on sync, not the offline moment. Knowing that avoids surprises when offline writes land.
Use the field-transform helpers: increment(n) adds to a numeric field, and arrayUnion / arrayRemove add or remove array elements, all server-side and atomic. Concurrent increments from many clients all apply correctly without a transaction.
These are cheaper and simpler than a transaction for the common cases. Reach for a transaction only when the new value depends on reading and validating current data first.
import { doc, updateDoc, increment, arrayUnion } from "firebase/firestore";
await updateDoc(doc(db, "posts", id), {
likes: increment(1), // atomic counter
likedBy: arrayUnion(currentUid), // atomic array add
});Two common approaches. Custom claims: set a role like admin: true on the user's auth token with the Admin SDK from a trusted server or function. Rules then read request.auth.token.admin directly. Or store roles in a Firestore document and read it in rules with get().
Custom claims are fast (no extra read) but limited in size and cached in the token until refresh. Firestore role documents update instantly but cost a read per rule check. Naming that trade-off is the intermediate-level answer.
// server side, Admin SDK
await admin.auth().setCustomUserClaims(uid, { admin: true });
// in security rules
// allow write: if request.auth.token.admin == true;The Emulator Suite runs Firestore, Auth, Functions, Storage, and more locally on your machine. You develop and test against emulators instead of production, so you don't pollute real data, don't pay for reads, and can run automated tests, including security-rules tests, in CI.
It's the standard answer to 'how do you test Firebase code?'. Rules and functions get tested against the emulator; the client points at it with connectFirestoreEmulator during development.
firebase emulators:start --only firestore,auth,functions
# app connects to localhost instead of productionStorage has its own rules with syntax close to Firestore's. You match file paths and allow read or write based on request.auth and file metadata like size and content type. A typical rule lets a user write only to their own folder and caps file size.
Validating content type and size in rules matters because it's your only server-side guard against someone uploading a 2 GB file or a script disguised as an image.
match /avatars/{uid}/{file} {
allow write: if request.auth.uid == uid
&& request.resource.size < 5 * 1024 * 1024 // under 5 MB
&& request.resource.contentType.matches('image/.*');
}A plain HTTPS function is a raw endpoint you handle request and response for, good for webhooks and public APIs. A callable function is a special HTTPS function invoked through the Firebase SDK: it automatically passes the user's auth token, handles CORS, and serializes data both ways.
Use callables for app-to-backend calls where you want the signed-in user's identity for free. Use raw HTTPS for third-party webhooks (like Stripe) that don't speak the callable protocol.
// callable: auth context is automatic
import { onCall } from "firebase-functions/v2/https";
export const setNickname = onCall((request) => {
const uid = request.auth.uid; // already verified
return saveNickname(uid, request.data.name);
});Fan-out means writing the same data to multiple places so reads are cheap and single-query. A new post might be written to the author's feed and every follower's feed. Reads become trivial; the follower just reads their own feed collection.
The cost is write amplification and consistency: one action becomes many writes, usually run in a Cloud Function or batch, and you have to keep the copies in sync when data changes. It's the classic NoSQL trade: pay at write time to be fast and cheap at read time.
A listener bills a read for every document delivered: the initial result set, plus one read per document each time it changes. A broad listener on a busy collection can generate reads continuously, which is where surprise bills come from.
Keep it in check by scoping queries tightly (filter and limit), unsubscribing when a view closes, and preferring one document you can watch over a wide query where possible. Listener discipline is a real cost lever, not a micro-optimization.
App Check verifies that requests to your backend come from your genuine app, not a bot or a script hitting your API directly. It uses attestation providers (reCAPTCHA on web, Play Integrity on Android, App Attest on iOS) to issue a token that Firebase services can require.
It complements security rules: rules decide who can access what, App Check helps ensure the request came from your real app in the first place. It doesn't replace rules; it reduces abuse and scraping.
Firestore has no joins, no native full-text search, limited OR support, and inequality filters were long restricted to a single field. You can't order by a field you're not also filtering on with an inequality without an index tweak. These constraints come straight from how it indexes data for scale.
The workarounds are practical: denormalize to avoid joins, push full-text search to a service like Algolia or Typesense synced by a function, precompute derived fields you want to filter on, and model documents around the exact queries a screen needs.
Key point: the key point is that you design queries first and shape the data to fit them, not the other way around.
Use onAuthStateChanged, a listener that fires whenever a user signs in, signs out, or when the SDK restores a session on page load. Your app reads the user (or null) from that callback and routes accordingly, rather than assuming a signed-in user synchronously.
The detail that trips people up is the initial state: on startup the SDK checks persisted credentials asynchronously, so there's a brief moment where the user is undefined before the first callback. Handle that loading state instead of flashing a login screen.
import { getAuth, onAuthStateChanged } from "firebase/auth";
onAuthStateChanged(getAuth(), (user) => {
if (user) showApp(user.uid);
else showLogin(); // also fires after sign-out
});Store secrets in Cloud Secret Manager and bind them to a function so they're injected at runtime, never in your source or repo. Non-secret settings come from environment variables or parameters defined in code. The old functions.config() approach is deprecated in favor of this.
The point to make: API keys for third-party services live server-side in a function with a bound secret, which is exactly why that logic can't live on the public client.
import { onRequest } from "firebase-functions/v2/https";
import { defineSecret } from "firebase-functions/params";
const STRIPE_KEY = defineSecret("STRIPE_KEY");
export const charge = onRequest({ secrets: [STRIPE_KEY] }, (req, res) => {
const key = STRIPE_KEY.value(); // injected at runtime
});advanced rounds probe scaling, cost, security depth, and production scars. Expect every answer here to draw a follow-up.
Firestore scales reads and storage well, but writes have limits worth designing around. A single document has a sustained write ceiling of roughly one write per second, and monotonically increasing indexed fields (like a plain timestamp used as a sort key at high volume) create index hotspots.
You design around it by sharding hot counters across several documents and summing, by adding randomness or a hashed prefix to sequential keys to spread index load, and by moving heavy aggregation into precomputed summary documents. Knowing these specific limits, not just 'it scales', is the production signal.
Key point: The distributed-counter (sharded counter) pattern is the answer they're fishing for when they ask about a high-traffic like button.
Because one document caps around a write per second, you shard the counter: split it into N documents, each holding a piece of the total. Writes pick a random shard to increment, so throughput multiplies by N. Reads sum all shards.
You trade a slightly more expensive read (N document reads) for far higher write throughput. For display values that tolerate slight lag, a Cloud Function can periodically roll the shards into one summary document so clients read a single number.
import { doc, updateDoc, increment } from "firebase/firestore";
const NUM_SHARDS = 10;
function bump(counterId) {
const shard = Math.floor(Math.random() * NUM_SHARDS);
return updateDoc(doc(db, `counters/${counterId}/shards/${shard}`), {
count: increment(1),
});
}
// total = sum of count across all shard docsA cold start is the latency when a new function instance spins up: loading the runtime, your code, and dependencies before handling the request. It hits low-traffic functions and after scale-to-zero. You reduce it by setting minInstances to keep warm instances, trimming dependencies and top-level imports, and choosing a lighter runtime.
The judgment call is cost versus latency: minInstances keeps instances warm but you pay for idle time. For user-facing paths that need consistent latency it's worth it; for rare background jobs it isn't.
Cutting cold-start latency, in order of impact
Measure first. Most cold-start pain comes from a bloated dependency graph, not from Firebase itself.
Since billing is per read, write, and delete, the biggest lever is reading fewer documents. Precompute aggregates into summary documents instead of reading a whole collection to count or total it. Cache on the client and use offline persistence to serve repeat reads locally. Scope every listener tightly and unsubscribe.
On writes, batch related writes, avoid rewriting whole documents when a field update suffices, and watch fan-out amplification. The senior habit is treating each screen as a read budget and designing the data model to hit it in as few indicates possible.
For high-frequency, low-latency, small-payload state where Firestore's per-operation cost and slightly higher latency hurt: presence (who's online), live cursors, typing indicators, and simple synced counters. The Realtime Database's flat tree and per-GB pricing fit that write-heavy, tiny-value pattern better.
Many production apps run both: Firestore as the primary store, the Realtime Database for presence and ephemeral live state. Naming that split, rather than treating it as either/or, is the experienced answer.
| Need | Better choice | Why |
|---|---|---|
| Rich queries, large data | Firestore | Compound queries, automatic scaling |
| Presence / online status | Realtime Database | onDisconnect and cheap tiny writes |
| Complex security rules | Firestore | Granular per-document conditions |
| Very high-frequency small updates | Realtime Database | Per-GB pricing suits tiny frequent writes |
Deny by default and open narrowly: rules that start locked and grant specific reads and writes with ownership and validation checks, tested in CI with the rules emulator so a regression can't ship. Validate incoming data shape in rules, not just identity. Turn on App Check to cut abuse from outside your app.
Beyond rules: keep all secrets in Secret Manager and privileged logic in functions using the Admin SDK (which bypasses rules, so those functions are the trusted boundary), enforce least privilege on service accounts, and monitor for anomalous read spikes that signal scraping.
Key point: Saying the Admin SDK bypasses security rules, and therefore server code is the trust boundary, is the line that marks a senior security answer.
The client SDK runs in the user's app and is subject to security rules: it can only do what rules allow for the signed-in user. The Admin SDK runs in a trusted server environment (a function, your backend) with full privileges and bypasses security rules entirely.
That difference defines your trust boundary. Anything that must be trusted, setting custom claims, privileged writes, cross-user operations, runs through the Admin SDK on the server, never the client.
import { getFirestore } from "firebase-admin/firestore";
// server side: bypasses security rules, so guard access yourself
const db = getFirestore();
await db.doc(`users/${uid}`).update({ verified: true });There's no ALTER TABLE, so migrations are code: a script or Cloud Function that reads documents in batches, transforms them, and writes them back, usually paginated by cursor to avoid loading everything at once. You make changes backward-compatible so old and new clients both work during rollout.
For large collections you run it as a batched, resumable job (Cloud Functions or a dedicated task) and often keep a schemaVersion field per document so you can migrate lazily on read as well. Idempotency matters because you may need to re-run it.
Firestore gives strong consistency for document reads and queries: a read reflects the latest committed write, and queries are served from synchronously updated indexes, so you don't see stale data the way an eventually consistent store might. Transactions are serializable through optimistic concurrency with retries.
The nuance is offline and listeners: a local cache can serve a slightly stale value with metadata marking it from-cache and pending writes, which resolve on sync. Distinguishing the strong server-side model from local cache behavior is the precise answer.
Cloud Functions logs and traces flow into Cloud Logging and Cloud Monitoring, where you set alerts on error rate, latency, and invocation counts. Firestore usage dashboards show read/write/delete volume so you can catch a query change that quadruples reads. Structured (JSON) logs make functions searchable by field.
The production discipline: correlate a read-cost spike or latency regression with the deploy that caused it, add alerts before scale rather than after the bill, and use the emulator to reproduce locally. Observe, correlate, fix, confirm, the same method any backend rewards.
It's real: security rules, the offline sync model, and the query semantics don't port to another database, and rewriting them is significant work. Data itself exports (Firestore has managed export to Cloud Storage), but the behavior around it, real-time listeners, rules, functions, is Firebase-specific.
Mitigation is architectural, not magical: keep business logic in your own code behind a thin data-access layer rather than sprinkling SDK calls everywhere, so swapping the store is a bounded change. Go in clear-eyed; the productivity is worth the coupling for many products, but say the trade-out loud.
Firestore writes index entries in sorted order. If many documents share a sequentially increasing indexed value (an auto-incrementing key or a raw timestamp under heavy write load), their index writes concentrate on the same range and throughput collapses. That's hotspotting.
You avoid it by not indexing high-write sequential fields, or by spreading them: prefix keys with a hash or shard, or disable indexing on fields you never query. The rule of 500/50/5, ramp write rates gradually to a new collection, comes from the same underlying behavior.
Firestore has server-side aggregation queries, count(), sum(), and average(), that return a result without reading every matching document, and they bill far less than reading the collection. Use those for on-demand totals.
For values shown constantly or on hot paths, precompute: maintain a running total in a summary document via a Cloud Function or atomic increment on each write, so the read is a single document. Reading a whole collection to count it is the anti-pattern this question checks against.
import { collection, query, where, getCountFromServer } from "firebase/firestore";
const q = query(collection(db, "orders"), where("status", "==", "paid"));
const snap = await getCountFromServer(q);
console.log(snap.data().count); // no per-document readsYou pick a Firestore location once, at database creation, and it's permanent. Multi-region locations give higher availability and durability by replicating across regions, at higher cost and slightly higher write latency. Regional locations are cheaper and lower-latency but tied to one region's availability.
The decisions that follow: choose the location near your users and other Google Cloud resources to cut latency, and because it can't change later, get it right up front. For global apps you weigh multi-region durability against the write-latency and cost hit.
The common shape: the client calls a callable Cloud Function, the function calls a model API (Vertex AI or another provider) with a server-held key, and results plus metadata get written back to Firestore, where a listener streams them to the UI in real time. Secrets and rate limiting stay server-side.
Firebase also offers Genkit and Vertex AI integrations so functions can call models with less glue. The point for an interview is the pattern, not the product: model calls belong in trusted functions, Firestore carries the results and history, listeners deliver them live.
Upload straight to Cloud Storage from the client using resumable uploads for big files, gated by Storage security rules that check size and content type. Serve downloads through the CDN via download URLs or, for private files, short-lived signed URLs minted by a function.
For processing (thumbnails, transcoding, virus scan), trigger a Cloud Function on the finalize event rather than doing work on the client. Never route file bytes through Firestore; Firestore holds the metadata and the Storage path, not the file.
For rules, the @firebase/rules-unit-testing library runs against the Firestore emulator: you assert that a given auth context can or can't perform each operation, so allowed and denied paths are both covered. For functions, you invoke them against the emulator with fixture data and assert on the resulting writes and responses.
Wiring this into CI means a rule or function change that opens a hole or breaks a flow fails the build. Saying you test the denied cases, not just the happy path, is the detail that lands.
import { assertFails, assertSucceeds } from "@firebase/rules-unit-testing";
// alice can read her own doc
await assertSucceeds(aliceDb.doc("users/alice").get());
// but not bob's
await assertFails(aliceDb.doc("users/bob").get());The recurring ones: leaving security rules open (allow read, write: if true) past a prototype, reading whole collections client-side just to count or filter them, forgetting to unsubscribe listeners so reads pile up, stuffing unbounded lists into one document until it nears the 1 MB cap, and orphaning subcollections on delete.
Underneath them all is one habit gap: not treating indicates a metered cost and rules as the real security layer. A candidate who can list these from experience, and say how each surfaces (a bill spike, a leak, a memory climb), indicates someone who has shipped and been burned.
Key point: This question rewards specifics. Vague 'watch your costs' loses to naming the exact anti-pattern and how you caught it.
Watch a deeper explanation
Video: 100 Firebase Tips, Tricks, and Screw-ups (Fireship, YouTube)
When the workload is heavily relational with complex joins and ad hoc analytical queries, a SQL database fits better and Firestore forces awkward denormalization. When read volume is enormous and predictable, per-operation pricing can cost more than a provisioned database. When you need full control over the runtime, region behavior, or want to avoid vendor coupling, running your own stack wins.
The mature answer isn't 'Firebase is bad'; it's matching the tool to the shape of the data and the team. Firebase trades control and relational power for speed and managed operations, and you say which side of that trade the project is on.
Key point: Interviewers ask this to see if you can criticize your favorite tool. A candidate who can't name where Firebase is the wrong choice indicates inexperienced.
Firebase wins when you want a real-time, offline-capable backend live in hours with almost no server code, which fits mobile apps, prototypes, and small-to-mid products. It trades SQL joins and full control for speed and a managed runtime, so you model data around access patterns rather than normalized tables. Where it strains is heavy relational querying, complex aggregations, and cost predictability at very high read volumes, where a SQL-backed option or your own service fits better. Saying these trade-offs out loud is itself an interview signal: it shows you pick a backend on merits, not habit.
| Option | Database model | Best at | Watch out for |
|---|---|---|---|
| Firebase | NoSQL (document / tree) | Real-time apps, mobile, fast start | No SQL joins, per-read cost at scale |
| Supabase | PostgreSQL (relational) | SQL power with real-time add-ons | Younger ecosystem, you manage more schema |
| AWS Amplify | Pluggable (DynamoDB, others) | Deep AWS integration, GraphQL API | Steeper setup, more moving parts |
| Custom backend | Your choice | Full control, any query shape | You build and run auth, sync, scaling yourself |
Prepare in layers, and practice out loud. Most Firebase rounds move from concept questions to a small build or data-modeling task to a security-rules or cost discussion, so rehearse each stage rather than only reading answers.
The typical Firebase interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Firebase questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works