4 of the 7 top posts for 2021 on the Firebase subreddit are about heart attack inducing surprise costs.
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.
- Use Google PubSub to track your Firebase spending
- Create a Firebase Function to listen to the billing PubSub
- Use the Firebase Function to remove billing from the project (i.e. pull the plug)
To track and cap the costs of a Firebase project you need the proper permissions. More specifically:
- access to edit the billing account GCP billing access
- access to create PubSub topics GCP Pubsub
- Cloud Billing API enabled GCP APIs & Services
...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.
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:
- Click on the cog icon and go to Usage and billing
- Select the Details & settings tab
- Click on View budgets to go to Budgets & alerts in your GCP console
- Select your project from the list
- Scroll to the bottom and select the Connect a Pub/Sub topic to this budget checkbox
- 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.)
- 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.
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.
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:
- set up credentials to be able to talk to
Google Billing API
- implement the function that uses
Google Billing API
to remove the billing account - update the
billingMonitor
pubsub function to calldisableBilling
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
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 })
}
// ...
// ...
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.
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
})
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:
It usually takes a few minutes for the Firebase dashboard to reflect the downgrade to the Spark
(free) tier.
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.