Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

How to Manage Costs in Firebase

Episode 125 written by Jeff Delaney
full courses for pro members

Health Check: This lesson was last reviewed on and tested with these packages:

  • Angular v6.1
  • RxJS v6.2
  • AngularFire2 v5.0.0-rc.11

Find an issue? Let's fix it

Source code for How to Manage Costs in Firebase on Github

The cloud can be expensive, especially if you fail to optimize your app for the provider’s pricing model. What’s worse is that you won’t feel pricing spikes until your app gets popular, at which point your life will be in total chaos. When your app starts going viral, your costs will increase by the second, so it’s best to make these optimizations early in the game.

This lesson was inspired by a recent article How we spent 30k USD in Firebase in less than 72 hours by Nicholas Contreras. Give it a read, but basically they had one bad line of code that caused their Firestore reads to go into the multi-billion range with about 2 million daily active users.

A demo for controling costs in Firebase and Firestore

Step 1 - Use Budget Alerts

Get notified before your costs grow out of control by setting up a budget alert in the GCP console. You don’t want to wait a full month if your app is accruing costs at $600/hr.

how to setup a gcp budget alert

Step 2 - Clientside Optimization

The first place to make optimizations is on the client. An optimized app should make only one request to Firebase per unique data source - don’t make redundant requests to the backend for the same data. It’s common to see multiple components request the same document or collection, each firing off their own read operation to Firestore. When you scale up to millions of requests, these extra reads will double your baseline costs N times, where N is the number of redundant reads that exist in your code.

Bad Code

The code below creates three different Observables, even though they all rely on the same donations collection.


// First
this.donations = afs.collection('donations').valueChanges()

// Second
this.total = afs.collection('donations').valueChanges().pipe(
map(arr => arr.reduce((total, current) => total + current.amount, 0))
);

// Third
this.average = afs.collection('donations').valueChanges().pipe(
map(
arr =>
arr.reduce((total, current) => total + current.amount, 0) / arr.length
)

Optimized Code

RxJS and Angular makes it easy to share data among multiple subscribers. By just piping in the shareReplay operator we can multicast and cache the last emitted value with as many subscribers as we need.

Need to share data among multiple components? Create this Observable in a service and inject it as singleton.

// Source
this.source = afs.collection('donations').valueChanges().pipe(shareReplay(1));

// Sharing
this.total = this.source.pipe(...);

// Sharing
this.average = this.source.pipe(...)

Outside of RxJS, there are other ways to achieve similar behavior. But the general idea is to make requests to Firebase once, then share this single source across all views that need it.

Step 3 - Data Aggregation

The most powerful cost optimizations you can make are on the server by duplicating and aggregating data to fit the UI live a glove. A common strategy is to aggregate statistics about a collection after a new document is added to it. You can also duplicate a handful of documents into an embedded array to have a single document that contains the last 10 documents, thus reducing ten reads per user to just one.

You might be surprised at how easy this is to implement with a Cloud Function. Let’s imagine we have a collection of donations, but our UI needs to show the total amount and average donation across this collection. The function below will run after each new donation is created, then update a separate document with the new totals. The aggregation doc is what we read in the UI.

const functions = require('firebase-functions');
const admin = require('firebase-admin');

admin.initializeApp()
const db = admin.firestore();

exports.aggregate = functions.firestore
.document('donations/{donationId}', ).onCreate(async(snapshot, context) => {

const donation = snapshot.data();

const aggRef = db.doc('aggregation/donations');

const aggDoc = await aggRef.get();
const aggData = aggDoc.data();


// Aggregate New Data

const next = {
total: aggData.total + donation.amount,
count: aggData.count + 1,
last5: [donation, ...aggData.last5.slice(0, 4)]
}

return aggRef.set(next)

});

We now only need one document read per visitor, as opposed thousands per visitor when totaling up the collection clientside. Not only is this faster, but it could save tens of thousands of dollars in read costs.

Had the startup company used aggregation from the start, their Firebase costs would have been reduced from $30K to just $25 - a savings of 99.9%.

Pricing on Blaze updated for data aggregation plan