Back to blog
Backend Development 9 min read14 July 2026

Migrating from Netlify Functions to Firebase: Real Cost, Timeline, and What Broke

We moved our own production backend off Netlify Functions onto Firebase Cloud Functions. Here's the actual timeline, the bugs that shipped, and what we'd do differently.

Firebase Netlify Cloud Functions Migration DevOps
H

Hanuman Singh

Founder & Lead Engineer · Hanuman Software Services

Why we're the right people to write this

This isn't a hypothetical migration guide. This is the backend behind hanumansingh.dev itself — our own contact form, booking system, and inbound email forwarding. We built it on Netlify Functions first, hit real limits, and moved it to Firebase Cloud Functions over about two weeks. Every timestamp, every bug, and every fix in this post is from our own commit history, not a sanitised case study.

Why we left Netlify Functions

Netlify Functions worked fine at first — five endpoints (contact, booking, availability, slots, inbound email forwarding) backed by Firestore, all running as serverless functions bundled with esbuild. The problems showed up as the functions grew past trivial:

  • Native binary resolution kept breaking. A Mac-generated package-lock.json caused Netlify's Linux build environment to fail looking for @rollup/rollup-linux-x64-gnu. Fixed by deleting the lockfile and letting Netlify regenerate it — a fragile fix, not a real one.
  • firebase-admin bundling fought esbuild for weeks. Three separate fixes, each addressing a different symptom of the same root problem: esbuild wants to inline dependencies, but firebase-admin and @google-cloud/firestore ship native binaries that can't be bundled. We had to explicitly mark them as external_node_modules, switch from ES imports to require() so esbuild treated the package as a true runtime dependency, and then switch again to firebase-admin/app / firebase-admin/firestore subpath imports because esbuild was resolving the wrong CJS export.
  • The Admin SDK crashed under cold starts. initializeApp() was called lazily inside our Firestore helper, which raced across concurrent cold-start invocations sharing a function instance — sometimes throwing "The default Firebase app does not exist" in production, non-deterministically.
  • Manual reCAPTCHA JWT minting. Without first-class GCP service integration, verifying reCAPTCHA Enterprise tokens meant hand-rolling a signed JWT for the service account rather than using a client library.

None of these were unsolvable. But we were spending more time fighting the deploy platform than building features, and every fix was a workaround for the fact that Netlify Functions is a generic Node runtime, not one built around the GCP/Firebase ecosystem our functions actually depend on.

The actual timeline

Real commit timestamps from our own repository, not rounded estimates:

  • Days 1–10: Netlify Functions built out and hardened in production — reCAPTCHA, rate limiting, Svix webhook verification, email templates. This is the baseline the migration replaced, not part of the migration cost.
  • Day 11: Firebase Hosting + Cloud Functions (2nd gen) scaffolded. All five functions ported to functions/src/ in a single focused session — switching Firestore access to Application Default Credentials and reCAPTCHA verification to google-auth-library instead of manual JWT minting. Old Netlify config deliberately left in place pending a verified preview deploy.
  • Day 11, later: First production bug — "The default Firebase app does not exist" on every Firestore-backed function. Root cause: the exact same lazy-initialization race that had bitten us on Netlify, ported over unchanged. Fixed by moving initializeApp() to module load time.
  • Day 11–12: Firebase App Check added, then migrated every function from onRequest to onCall — trading hand-rolled header parsing and CORS handling for built-in App Check enforcement, replay protection, and typed request/response handling. Also replaced the in-memory rate limiter (silently broken across the multiple Cloud Run instances a function can scale to) with a Firestore-backed counter.
  • Day 12: Second app-initialization bug, different root cause. availableDates and slots called App Check verification before touching Firestore, so firebase-admin had never been initialized when the App Check check ran — contact and book only worked "by accident" because their Firestore call happened to initialize the app first. Fixed by initializing defensively inside the shared helper instead of relying on call order.

Total: roughly two working days once we committed to the migration, spread across three focused sessions. The bugs weren't from Firebase being fragile — two of them were pre-existing Netlify bugs we ported over unchanged, and the third was App Check's stricter execution-order requirements surfacing a latent ordering assumption in our own code.

What actually got better

  • App Check replaced a hand-rolled origin check. Netlify Functions verified the request origin manually; onCall functions get App Check enforcement, replay protection, and CORS handling as defaults, not code we maintain.
  • The rate limiter became actually correct. The in-memory counter on Netlify looked fine in testing and was silently useless in production — each Cloud Run instance had its own counter, so five requests could hit five different instances and all pass. The Firestore-backed version is correct under real concurrent load, verified via emulator-driven tests including duplicate-slot rejection.
  • One local dev loop instead of two. Firebase's emulator suite (Functions + Firestore + Hosting) replaced Netlify Dev, and it's the same tool we deploy with — no more "works in Netlify Dev, breaks in production" gaps.
  • Native dependency handling stopped being our problem. Firebase's Node 22 runtime handles firebase-admin's native binaries natively — the three esbuild bundling fixes we needed on Netlify simply don't apply.

What we'd tell you before you migrate

  1. Port your bugs, not just your code. Our first production incident after migrating was a bug we already had on Netlify, copy-pasted into the new platform. Review lazy-initialization and call-order assumptions specifically — they don't announce themselves until concurrency exposes them.
  2. Budget a session for the App Check migration, not just the port. If your functions currently do manual origin/auth checking, moving to onCall is worth doing at the same time as the platform migration — but it's real design work, not a config flag.
  3. Test rate limiting under actual concurrency before you trust it. An in-memory counter will pass every manual test and fail exactly when you need it — under real traffic hitting multiple instances at once.
  4. Keep the old platform live until you've verified a full production cycle. We didn't cut DNS over until the new functions had handled real booking and contact submissions end-to-end through the emulator and a preview deploy.

Is this migration worth it for you?

If you're already deep in the Firebase/GCP ecosystem — Firestore, App Check, Cloud Functions elsewhere — consolidating onto Firebase Functions removes a whole category of native-dependency bundling pain. If you're not, Netlify Functions remains a perfectly reasonable default; most of what we hit was specific to running firebase-admin outside its native runtime, not a general indictment of serverless-on-Netlify.

Thinking about a similar migration, or debugging one of these exact errors right now? Tell us what's breaking and we'll tell you honestly whether the move is worth it for your stack.

Interested in working together?

Free 30-minute discovery call — no commitment.

Book a call