Skip to content

Firebase billing surprises: how to really cap your spending

firebasefirebase functions

4 of the 7 top posts for 2021 on the Firebase subreddit are about heart attack inducing surprise costs.

top stories from firebase subreddit

The official docs have an article titled “How to avoid billing surprises” which suggest you should set up an email alert based on your spend.

As someone who has blown through his mobile data plan thinking he was using Wi-Fi, I can tell you that budget alerts are useless and usually arrive too late. SURPRISE!

Back in 2019 someone decided to take out the fixed price plan from Firebase and thus we're now left with the Free plan, with its limitations, and the Blaze — pay as you go plan, with absolutely no limitations. Oh, you're project is eating up $10k/day? We're Google, those numbers seem low.

And that's the crux of the matter. With no way to hard limit spend, there really isn't a way to avoid surprise billings.

A few years back this story went viral about how a Firebase project burnt through $30.000 in just a few days because of a minor mistake in implementation. So let's see how you can avoid becoming the next cautionary tale.

The process is a bit convoluted. I'm not saying it's a dark pattern. I'm not saying it isn't.

solution overview

  1. Use Google PubSub to track your Firebase spending
  2. Create a Firebase Function to listen to the billing PubSub
  3. Use the Firebase Function to remove billing from the project (i.e. pull the plug)

prerequisites

To track and cap the costs of a Firebase project you need the proper permissions. More specifically:

...and the obligatory disclaimer:

🚨 CAUTION 🚨
Pulling the cord will make your Firebase project immediately stop any process that doesn't fit in the Free tier.
Proceed with caution.

1. Use PubSub to track your Firebase projects’ cost

If you’re unfamiliar with it, PubSub is a simple messaging service.

Pub/Sub enables you to create systems of event producers and consumers, called publishers and subscribers. Publishers communicate with subscribers asynchronously by broadcasting events.
(source: Google Cloud docs)

In this case PubSub will track and broadcast the Firebase project cost.

To set up the billing tracking PubSub you need to go over into Google Compute Platform (GCP) dashboard. From your project’s Firebase dashboard:

  1. Click on the cog icon and go to Usage and billing
  2. Select the Details & settings tab
  3. Click on View budgets to go to Budgets & alerts in your GCP console
  4. Select your project from the list
  5. Scroll to the bottom and select the Connect a Pub/Sub topic to this budget checkbox
  6. Scroll down some more and from the dropdown dialog select or create a topic (note: If the Create Topic button is grayed out, click on Switch Project and make sure the correct project is selected.)
  7. Click the ‘Save’ button at the bottom of the page

PubSub usually takes several hours to send out the first message. But from then on it sends messages about once every 20 minutes.

2. Create a Firebase Function to listen to the billing PubSub

The next milestone is to create a Firebase Function that listens for the billing PubSub and logs the current budget spend of the project.

Assuming you already have the Firebase Function project set up, you can jump into code and create an empty function that listens to the topic you just created: billing-killa-budget.

// /src/index.ts
import * as functions from 'firebase-functions'

export const billingMonitor = functions.pubsub.topic('billing-killa-budget').onPublish((message) => {
	console.log('Got a pubsub message')
	return null // returns nothing
})

Most likely, you’ve not used the pubsub module so far and you need to install it by running:

npm install @google-cloud/pubsub

If you’d like to test your PubSub ↔ Firebase Functions integration locally check out the tutorial on publishing messages to the local Pubsub emulator.

A sample message from the billing PubSub topic might look like this:

{
	"budgetDisplayName": "billing-killa",
	"costAmount": 1.25,
	"costIntervalStart": "2022-01-28T00:00:00Z",
	"budgetAmount": 5,
	"budgetAmountType": "SPECIFIED_AMOUNT",
	"currencyCode": "USD"
}

Now you can update your function to be a bit more useful. To print out the current cost of your project, update the function to:

//...

export const billingMonitor = functions.pubsub.topic('billing-killa-budget').onPublish((pubsubMessage) => {
	const pubsubData = JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString())
	const { costAmount, currencyCode } = pubsubData

	functions.logger.info(`Project current cost is: ${costAmount}${currencyCode}`)

	return null // returns nothing
})

Deploy your function to Firebase to see it in action.

To test it you can manually publish the above sample message to your PubSub topic. You should get the following output in your Firebase Functions logs.

Firebase Functions Logs

3. Use the Firebase Function to remove billing

And now for the grand finale! 🎆

You’ll write a function that uses Google Billing API to remove your billing account and thus putting a cap on your spend.

The quick overview of this step is as follows:

  1. set up credentials to be able to talk to Google Billing API
  2. implement the function that uses Google Billing API to remove the billing account
  3. update the billingMonitor pubsub function to call disableBilling when needed

🚨 CAUTION 🚨
This is not a drill! When you remove your billing account, your Firebase project is switched to the Free tier and all resources that don’t fit in that tier are stopped immediately.This is not a graceful shut down, so proceed with cautio

3.1. Set up credentials with GoogleAuth

For this function you’ll need the google-auth-library module.

npm install google-auth-library

Then you can implement

// ...
import { GoogleAuth } from 'google-auth-library'

// ...

const _setAuthCredentials = () => {
	const client = new GoogleAuth({
		scopes: ['https://www.googleapis.com/auth/cloud-billing', 'https://www.googleapis.com/auth/cloud-platform'],
	})

	google.options({ auth: client })
}

// ...

3.2. Create a function to cut off the billing on the Firebase project

// ...
import { google } from 'googleapis'

const billing = google.cloudbilling('v1').projects
const PROJECT_ID = process.env.GCLOUD_PROJECT
const PROJECT_NAME = `projects/${PROJECT_ID}`

// ...

const disableBilling = async () => {
	_setAuthCredentials()

	if (PROJECT_NAME) {
		const billingInfo = await billing.getBillingInfo({ name: PROJECT_NAME })
		if (billingInfo.data.billingEnabled) {
			try {
				await billing.updateBillingInfo({
					name: PROJECT_NAME,
					requestBody: { billingAccountName: '' },
				})
				functions.logger.info(`✂️ ${PROJECT_NAME} billing account has been removed`)
			} catch (error) {
				functions.logger.error(error)
			}
		} else {
			console.log('👉 looks like you already disabled billing')
		}
	}
}

// ...

Notice this function calls _setAuthCredentials defined in the previous step. It then attempts to update the billing info to an empty account name.

You’re also importing the googleapis module so you’ll need to install it by running:

npm install googleapis

Make sure to enable Google Billing API for your project. Go to https://console.cloud.google.com/apis/, click on Enable APIs and Services and search for Google Billing API and enable it.

3.3. Update the pubsub function to call [object Object] when the cost exceeds the set budget

export const billingMonitor = functions.pubsub.topic('billing-killa-budget').onPublish(async (pubsubMessage) => {
	const pubsubData = JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString())
	const { costAmount, budgetAmount, currencyCode } = pubsubData

	functions.logger.info(`Project current cost is: ${costAmount}${currencyCode} out of ${budgetAmount}${currencyCode}`)
	if (budgetAmount < costAmount) await disableBilling()

	return null // returns nothing
})

deploy and test your code

At this point you should have all the coded needed and be ready to deploy and test your code.

For reference, the final code looks like this:

// src/index.ts
import * as functions from 'firebase-functions'
import { google } from 'googleapis'
import { GoogleAuth } from 'google-auth-library'

const billing = google.cloudbilling('v1').projects
const PROJECT_ID = process.env.GCLOUD_PROJECT
const PROJECT_NAME = `projects/${PROJECT_ID}`

export const billingMonitor = functions.pubsub.topic('billing-killa-budget').onPublish(async (pubsubMessage) => {
	const pubsubData = JSON.parse(Buffer.from(pubsubMessage.data, 'base64').toString())
	const { costAmount, budgetAmount, currencyCode } = pubsubData

	functions.logger.info(`Project current cost is: ${costAmount}${currencyCode} out of ${budgetAmount}${currencyCode}`)
	if (budgetAmount < costAmount) await _disableBilling()

	return null // returns nothing
})

const _setAuthCredentials = () => {
	const client = new GoogleAuth({
		scopes: ['https://www.googleapis.com/auth/cloud-billing', 'https://www.googleapis.com/auth/cloud-platform'],
	})
	google.options({ auth: client })
}

const _disableBilling = async () => {
	_setAuthCredentials()

	if (PROJECT_NAME) {
		const billingInfo = await billing.getBillingInfo({ name: PROJECT_NAME })
		if (billingInfo.data.billingEnabled) {
			try {
				await billing.updateBillingInfo({
					name: PROJECT_NAME,
					requestBody: { billingAccountName: '' },
				})
				functions.logger.info(`✂️ ${PROJECT_NAME} billing account has been removed`)
			} catch (error) {
				functions.logger.error(error)
			}
		} else {
			console.log('👉 looks like you already disabled billing')
		}
	}
}

It’s a good idea to wait for PubSub to send messages and see how your function handles them. But you can also test by manually sending PubSub messages.

Let’s simulate a budget amount of $100 and the cost going from $1 to $50 and then to $150, when we expect the function to trigger the fail-stop and our project to be downgraded to the Free tier. The expected outcome would be:

Firebase Functions Logs

It usually takes a few minutes for the Firebase dashboard to reflect the downgrade to the Spark (free) tier.

To DIY or not to DIY

YouTube comment

Yes, Julien, it probably should.

The goal of this tutorial is to fill in a gap in the official docs & guides. It’s still not a simple process unfortunately, and I believe that’s by design. If you need help implementing the billing cut off then reach out on Twitter.

Get updates for FREE

Put in your best email and I'll send you new articles, like this one, the moment they come out. ✌️

    Won't send you spam. Unsubscribe at any time.