How We Eliminated Cloudflare Workers Boot Crashes by Lazy-Loading Firebase SDK
Background
Building modern web apps often means reaching for a managed backend platform like Firebase for authentication, database, and storage — and a fast edge delivery network like Cloudflare Pages for serving the frontend. On paper, these two technologies are complementary. In practice, combining them exposed a subtle but devastating incompatibility that took us an entire debugging session to trace down.
This article documents exactly what went wrong, why it happened, and the architectural pattern we now use to safely run Firebase-dependent Next.js apps on Cloudflare Workers.
The Problem
We deployed our Next.js app to Cloudflare Pages using the OpenNext adapter with the edge runtime. The build succeeded cleanly. The deployment succeeded cleanly. But the moment we opened the site in a browser, every single route — including the homepage — returned a 500 error.
There were no build-time warnings. No TypeScript errors. No indication during deployment that anything was wrong. Just a blank page and a 500 in the network tab.
Checking Cloudflare's real-time logs surfaced this:
EvalError: Code generation from strings disallowed for this context
at new Function (<anonymous>)
And deeper in the stack:
at wn (/workers-site/index.js:1:...)
at Object.<anonymous> (/workers-site/index.js:1:...)
The Worker was crashing on startup — before it could handle even a single HTTP request. Every route returned 500 because the Worker process itself was dying before it reached any route handler.
Why Cloudflare Workers Block eval() and new Function()
Cloudflare Workers run inside V8 isolates — a sandboxed JavaScript execution environment that deliberately disables dynamic code generation for security reasons. This means:
eval("some code")— blockednew Function("return this")— blockedsetTimeout("some code", 0)— blocked
This restriction is enforced at the V8 level, not by a runtime check. There is no configuration option to disable it. It applies to all code running inside the Worker, including third-party library code bundled into the Worker file.
The restriction exists to prevent a class of attacks where injected strings are evaluated as code — XSS, prototype pollution, and similar. For most libraries this is a non-issue. For the Firebase SDK, it is a hard blocker.
Why Firebase Triggers This
The Firebase JavaScript SDK calls new Function('return this') during its initialisation sequence. This is a legacy pattern used to obtain a reference to the global object in environments where globalThis may not be available. The call happens at module evaluation time — not inside a function, not lazily, but immediately when the module is first imported.
In a browser or Node.js environment, this works fine because both allow dynamic code generation. In a Cloudflare Worker, it throws an EvalError that kills the Worker process.
The critical point is that this happens regardless of whether you ever actually call any Firebase function. The mere act of importing the Firebase module — even if the import is never used in that execution context — causes the SDK to run its initialisation code, which calls new Function(), which crashes the Worker.
Why Top-Level Imports Are the Root Cause
A typical Firebase setup in a Next.js project looks like this:
// lib/firebase.ts
import { initializeApp, getApps } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = { ... };
export const app = getApps().length === 0
? initializeApp(firebaseConfig)
: getApps()[0];
export const auth = getAuth(app);
export const db = getFirestore(app);
This pattern is widely documented and works perfectly in standard Next.js deployments. The problem is specific to how Cloudflare Workers execute code.
When OpenNext bundles your Next.js app for Cloudflare, it produces a single Worker file — handler.mjs or similar — that contains all your app code plus all its dependencies. When the Cloudflare runtime loads this Worker, it evaluates the entire module graph from top to bottom before handling any request.
Because firebase/app, firebase/auth, and firebase/firestore are top-level imports, the Firebase SDK modules are evaluated at startup. The new Function() call runs. The Worker crashes. No requests are ever served.
This happens even if the Firebase code is only used in client components marked with "use client". The bundler still includes those modules in the Worker bundle, and the Worker runtime still evaluates them at startup.
The Fix: Lazy require()
The solution is to prevent Firebase SDK modules from executing at Worker startup. We do this by replacing top-level import statements with require() calls placed inside function bodies. A require() inside a function is only evaluated when that function is called — never at module load time.
Here is our lib/firebase.ts after the fix:
// import type is erased at compile time — never included in the bundle
import type { FirebaseApp } from "firebase/app";
import type { Auth } from "firebase/auth";
import type { Firestore } from "firebase/firestore";
// Firebase SDK must be loaded lazily. Top-level imports cause the SDK to
// execute at module-evaluation time inside the Cloudflare Workers bundle,
// which crashes all routes with EvalError. require() here is intentional —
// it defers loading until the function is first called (always client-side).
/* eslint-disable @typescript-eslint/no-require-imports */
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
let _app: FirebaseApp | undefined;
let _auth: Auth | undefined;
let _db: Firestore | undefined;
export function getFirebaseApp(): FirebaseApp {
if (!_app) {
const { initializeApp, getApps } = require("firebase/app");
_app = getApps().length === 0
? initializeApp(firebaseConfig)
: getApps()[0];
}
return _app!;
}
export function getFirebaseAuth(): Auth {
if (!_auth) {
const { getAuth } = require("firebase/auth");
_auth = getAuth(getFirebaseApp());
}
return _auth!;
}
export function getFirebaseDb(): Firestore {
if (!_db) {
const { getFirestore } = require("firebase/firestore");
_db = getFirestore(getFirebaseApp());
}
return _db!;
}
The same pattern applies everywhere Firebase is used — in Firestore helpers, auth context, and any component that touches Firebase:
// Instead of:
import { doc, getDoc, setDoc } from "firebase/firestore";
// Use:
const { doc, getDoc, setDoc } = require("firebase/firestore");
// (inside a function body only — never at the top of the file)
For import type statements, no change is needed. Type imports are completely erased by the TypeScript compiler and never appear in the bundle — they are always safe.
Applying the Pattern in React Context
The pattern extends cleanly to React contexts. Here is how we handle onAuthStateChanged without triggering a Worker crash:
"use client";
/* eslint-disable @typescript-eslint/no-require-imports */
import type { User } from "firebase/auth";
import { createContext, useContext, useEffect, useState } from "react";
import { getFirebaseAuth, getFirebaseDb } from "@/lib/firebase";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
// require() inside useEffect — only runs in the browser, never in Worker
const { onAuthStateChanged } = require("firebase/auth");
const auth = getFirebaseAuth();
return onAuthStateChanged(auth, (u: User | null) => {
setUser(u);
});
}, []);
// ...
}
The useEffect only runs in the browser. The require() inside it only executes in the browser. The Worker never sees it.
For dynamic imports that are already async (like signInWithPopup), the native import() syntax is also safe because it is always asynchronous and never runs at module evaluation time:
async function loginWithGoogle() {
const { signInWithPopup, GoogleAuthProvider } = await import("firebase/auth");
// ...
}
Verifying the Fix
After rebuilding, you can verify that no new Function calls exist in the compiled Worker bundle:
grep -c "new Function" .open-next/worker.js
The count should be 0. If it is anything higher, a top-level Firebase import has crept back in somewhere.
You can also search for raw import statements referencing Firebase (excluding type imports):
grep -n "^import {" .open-next/worker.js | grep firebase
There should be no results. All Firebase references in the Worker bundle should come from require() calls inside function bodies.
ESLint Configuration
The @typescript-eslint/no-require-imports rule will flag these require() calls. Rather than disabling the rule globally — which would hide legitimate violations elsewhere — we disable it per-file with a comment that explains why:
// Firebase SDK must be loaded lazily — see firebase.ts for explanation.
/* eslint-disable @typescript-eslint/no-require-imports */
This keeps the intent visible to future contributors. Anyone reading the file sees immediately that the require() pattern is intentional, not accidental or outdated.
The Rule: Never Convert These Back
The most important operational note from this incident is a rule we now treat as a hard constraint:
Never convert the lazy
require()calls in Firebase files to top-level ES imports.
It is an easy mistake to make. ESLint's auto-fix for prefer-import rules will suggest converting require() to import. An AI coding assistant might "clean up" the pattern. A developer unfamiliar with the constraint might refactor it for consistency.
Any of these will silently re-introduce the crash. The build will succeed. CI will pass. The problem only manifests at runtime on Cloudflare, and only after deployment.
The file-level ESLint disable comment and the explanatory comment block are the primary defences against this. They signal clearly that the pattern is load-bearing and must not be changed.
The Broader Pattern
This issue is not specific to Firebase. Any library that calls eval(), new Function(), or Function.prototype.constructor during module initialisation will cause the same crash in Cloudflare Workers.
Common culprits include:
- Firebase SDK (all packages) —
new Function('return this')in core initialisation - Some analytics libraries — dynamic event handler generation
- Template engines — compile templates to functions at load time
- Older polyfills — global object detection patterns
The diagnostic signature is always the same: EvalError: Code generation from strings disallowed for this context in Cloudflare logs, with a stack trace pointing into a bundled dependency.
The fix is always the same: move the import inside a function body using require(), so the library code only executes when the function is called — in the browser, where dynamic code generation is permitted.
Summary
| Approach | Safe in Cloudflare Workers? |
|---|---|
import { x } from "firebase/..." at top level | No — crashes Worker on startup |
import type { X } from "firebase/..." at top level | Yes — erased at compile time |
const { x } = require("firebase/...") inside function | Yes — deferred until called |
const { x } = await import("firebase/...") inside async function | Yes — always asynchronous |
When you need Firebase in a Cloudflare Workers-hosted Next.js app, import type at the top and require() inside functions is the safe pattern. Everything else risks crashing your Worker before it handles a single request.