Skip to content
    2026-02-01|6 min read

    Serverless APIs with Firebase Cloud Functions: Patterns and Pitfalls

    #api#firebase#cloud-functions#serverless#nodejs

    Firebase Cloud Functions let you deploy serverless API code without managing infrastructure. The promise is seductive: write a function, deploy it, and it scales infinitely. The reality is more nuanced. Cold starts, dependency bloat, and runaway costs can turn the dream into a nightmare if you don't understand how the platform works.

    This guide covers production patterns for Cloud Functions and the pitfalls I've encountered running them at scale in production applications like PeptiSync and ProfitPlate.

    Cold Starts: The Silent Performance Killer

    When a Cloud Function hasn't been invoked for a while, the platform spins it down. The next invocation must cold-start — loading your runtime, dependencies, and function code before executing.

    Cold start latency depends on:

    FactorImpactMitigation
    RuntimeNode.js ~100ms, Python ~200ms, Java ~1sUse Node.js or Python
    Dependencies10MB = ~300ms, 50MB = ~1.5sMinimize dependencies
    Function countMore functions = slower cold startsCombine related logic
    RegionLess busy regions = faster cold startsChoose appropriate region

    Measuring Cold Starts

    Add cold start detection to your functions:

    ```typescript import * as functions from 'firebase-functions';

    let coldStart = true;

    export const myFunction = functions.https.onRequest(async (req, res) => { if (coldStart) { console.log('Cold start detected'); coldStart = false; } // Function logic }); ```

    Minimizing Cold Starts

    1. Keep instances warm:

    ``typescript export const criticalFunction = functions .runWith({ minInstances: 1, // Keep at least 1 instance always warm maxInstances: 10, // Cap at 10 to control costs }) .https.onRequest(async (req, res) => { // ... }); ``

    minInstances increases your bill (you pay for idle compute) but eliminates cold starts for critical endpoints.

    2. Reduce dependency footprint:

    ```typescript // Instead of importing the entire Firebase Admin SDK: import * as admin from 'firebase-admin'; // ~15MB admin.initializeApp();

    // Import only what you need: import { getFirestore } from 'firebase-admin/firestore'; import { getAuth } from 'firebase-admin/auth'; ```

    3. Use dependency caching:

    ```typescript // Bad: imported on every invocation for every function import { stripe } from './shared/stripe';

    // Good: lazy initialization let stripeClient: Stripe | null = null;

    function getStripe(): Stripe { if (!stripeClient) { stripeClient = new Stripe(functions.config().stripe.secret, { apiVersion: '2025-02-24.acacia', }); } return stripeClient; } ```

    Dependency Management

    Cloud Functions bundle your dependencies into the deployment package. Large packages increase cold start time.

    Better Approach: Function-Specific Dependencies

    Instead of a single package.json with everything:

    ``json // functions/package.json { "dependencies": { "firebase-admin": "^12.0.0", "firebase-functions": "^5.0.0" // Shared dependencies only } } ``

    Install function-specific packages only when needed:

    ``typescript // stripe-webhook/index.ts // Import heavy packages only in functions that need them import Stripe from 'stripe'; ``

    Use TypeScript Compilation

    Compile TypeScript to JavaScript before deploying to reduce bundle size:

    ``json { "predeploy": ["npm run build"] } ``

    Configure tsconfig.json to exclude tests and type definitions:

    ``json { "compilerOptions": { "outDir": "lib" }, "include": ["src"], "exclude": ["/*.test.ts", "/__tests__"] } ``

    Function Organization Patterns

    Single Responsibility Functions

    Each function should do one thing:

    ```typescript // Good export const createUser = functions.auth.user().onCreate(...); export const deleteUser = functions.auth.user().onDelete(...); export const handleStripeWebhook = functions.https.onRequest(...); export const processInvoice = functions.firestore .document('invoices/{id}') .onCreate(...);

    // Bad — generic Swiss Army knife export const api = functions.https.onRequest(async (req, res) => { switch (req.path) { case '/users': // ... case '/orders': // ... case '/payments': // ... } }); ```

    Express App for Complex APIs

    For multiple HTTP endpoints, use an Express app and export it as a single Cloud Function:

    ```typescript import as express from 'express'; import as functions from 'firebase-functions';

    const app = express();

    app.use(authenticate); app.use(rateLimit);

    app.get('/api/v1/users', getUsers); app.post('/api/v1/users', createUser); app.get('/api/v1/users/:id', getUserById); app.patch('/api/v1/users/:id', updateUser);

    export const api = functions.https.onRequest(app); ```

    This combines the cold start cost across all endpoints while keeping your code modular.

    Cost Optimization

    Function Execution Costs

    Firebase bills by compute time (GB-seconds) and invocations. The free tier covers 2M invocations per month, but heavy usage adds up quickly.

    OptimizationImpact
    Reduce timeout durationLower cost per invocation
    Use 256MB memory (not 1GB) for simple functions4x cheaper per second
    Cap maxInstancesPrevents cost spikes
    Use batch operationsFewer invocations

    Database and Egress Costs

    Every function that reads Firestore documents incurs read costs. Optimize by:

    ```typescript // Bad — reads per invocation export const getUserData = functions.https.onCall(async (data, context) => { const userDoc = await getDoc(doc(db, 'users', context.auth!.uid)); const postsSnap = await getDocs(query( collection(db, 'posts'), where('authorId', '==', context.auth!.uid) )); const statsSnap = await getDoc(doc(db, 'stats', context.auth!.uid));

    return { user: userDoc.data(), posts: postsSnap.docs.map(d => d.data()), stats: statsSnap.data() }; });

    // Better — aggregate data on write, read once export const aggregateUserData = functions.firestore .document('posts/{postId}') .onWrite(async (change, context) => { // When a post is created/updated, update the user's aggregate document }); ```

    Monitoring and Observability

    Error Reporting

    Cloud Functions integrates with Error Reporting:

    ```typescript import * as functions from 'firebase-functions';

    export const fragileFunction = functions.https.onRequest(async (req, res) => { try { // risky operation } catch (error) { console.error('Operation failed:', error); // Error Reporting automatically captures console.error res.status(500).json({ error: 'Internal server error' }); } }); ```

    Custom Metrics

    Log structured metrics for monitoring:

    ```typescript function logMetric(name: string, value: number, labels: Record<string, string> = {}) { console.log(JSON.stringify({ severity: 'INFO', metric: name, value, labels, timestamp: new Date().toISOString(), })); }

    // Usage export const processOrder = functions.firestore .document('orders/{orderId}') .onCreate(async (snap) => { const start = Date.now(); const order = snap.data();

    try { await chargeCustomer(order); logMetric('order_processing_ms', Date.now() - start, { status: 'success', tier: order.tier, }); } catch (error) { logMetric('order_processing_ms', Date.now() - start, { status: 'failed', error: error.message, }); } }); ```

    Timeout Handling

    Set appropriate timeouts to prevent runaway executions:

    ``typescript export const longRunningFunction = functions .runWith({ timeoutSeconds: 540, // Max: 9 minutes for HTTP functions memory: '1GB', }) .https.onRequest(async (req, res) => { // Heavy computation }); ``

    Pitfalls I've Learned the Hard Way

    1. Environment variables in config: Use functions.config() not .env files. Config is encrypted and version-controlled.
    2. Promise handling: Always return or await Promises in background functions. Unhandled promises cause silent failures.
    3. Region selection: Deploy functions close to your users. Firestore auto-scaling works best when functions are in the same region.
    4. Gradual rollouts: Use traffic settings in Google Cloud Run to gradually shift traffic to new function versions.
    5. Function naming: Once deployed, renaming a function creates a new one. Old URLs break if consumers cached them.

    Conclusion

    Firebase Cloud Functions are a powerful tool for building serverless APIs, but they require deliberate architectural decisions to perform well in production. Manage cold starts with minInstances for critical paths, keep dependencies lean, organize functions by responsibility, and monitor execution costs from day one. When treated with the same discipline as traditional backend services, Cloud Functions deliver on the serverless promise without the hidden surprises.

    Building a serverless API and want a review of your architecture? Let's talk about optimizing your Cloud Functions for production.

    ---

    R

    Written by

    Rahul

    Freelance developer for startups building SaaS products, MVPs, mobile apps, and conversion-focused website improvements.

    Building something?

    I am currently available for new projects. Share your idea and I will give you an honest assessment, delivery plan, and quote.