The shape of the problem.

Pick a public cloud and ask one question. If I set a $50/month budget on this project, what happens at $51?

On Azure, certain subscription types — Free Trial, Azure for Students, Visual Studio — ship with a real spending cap that disables resources when the cap is hit. On pay-as-you-go you wire the same behavior through a budget plus an Action Group plus an Automation runbook, and Microsoft's own docs walk you through it. The cap is in the box. The default for trial accounts is exactly what the word "budget" implies: they stop charging you.

On AWS and Google Cloud, "budget" means "we will email you." Google's own documentation says it out loud: "Setting a budget does not automatically cap Google Cloud usage or spending." AWS Budgets is the same shape — it fires SNS messages and EventBridge events; what happens after that is your problem to wire up. For a side studio that is the difference between a cloud bill and a credit-rating event.

Why AWS and Google won't ship the feature.

Their reasoning is fair, and it's the reason I don't entirely blame them.

Their highest-revenue customers run production workloads where the worst possible outcome of a cost spike is the bill. The second-worst possible outcome — the one a hard cap creates — is a self-inflicted outage triggered by your own billing system. For a real business, the moment a hard cap fires is the moment checkout goes down on the busiest shopping day of the year because traffic scaled faster than a budget meeting could agree to bump the cap. You stop taking money before you stop spending it. Nobody senior would sign that off.

So AWS and Google designed for the median enterprise customer: alert loudly, never auto-stop, let a human decide. It's the right default for a Fortune 500. It's a bad default for me.

The receipts.

The horror story everyone cites is real, and the numbers are better than anything I could invent. Sudeep Chauhan of Milkie Way set up a Firebase project to test Cloud Run. He set a billing budget of $7.00. Overnight, a recursive function ran what ended up being 116 billion Firestore read operations at $0.06 per 100,000 reads$69,600 in reads alone, plus 16,000 hours of Cloud Run compute on top. Total bill: $72,000. He woke up to an email from Google informing him that his free Firebase plan had been "upgraded due to activity in Google Cloud." Google eventually waived it.

"Google eventually waives it" is not a financial control. Bankruptcy-or-mercy is not an architecture. That story is from late 2020 and the shape of the problem hasn't changed since; search Hacker News or r/Firebase any month and a new variant shows up.

Why this hit me harder than I expected.

chitra.art uses Google Maps on the artist pages. To render Google Maps in a browser you embed a Google API key in the page's HTML. That key is, by design, public. There is no version of this where the key lives on the server.

Google's defense for the public-key model is restrictions: lock the key to specific HTTP referrers (your domains), lock it to a specific set of APIs (just Maps JavaScript, not the whole Google Cloud catalogue), and set per-API daily quotas. I did all of that. The key is restricted to *.chitra.art/* and localhost for dev; it's restricted to Maps JavaScript and Places only; per-API daily quotas are set to plausible numbers for the real traffic shape.

The catch is that referrer restrictions are checked from the Referer header, which a browser sends and a script does not have to. They're a strong hint, not a hard boundary. Quotas help — quotas are the closest thing Google offers to a real cap — but they're per-API and per-day, and the units they're denominated in (requests, sessions, autocomplete characters) don't map cleanly to dollars. The realistic threat model is: somebody scrapes the key out of the HTML, points it at the cheapest API I've enabled, runs it from a cloud function of their own until the quota tips, and either burns the day's allowance (best case: my map breaks) or finds an un-quota'd corner I forgot to lock (worst case: see Milkie Way).

The workaround.

The piece Azure ships and Google doesn't is "when the budget hits the cap, do the thing." On Google, you assemble it from parts.

Google Cloud budget configuration showing a Pub/Sub topic wired as the alert channel and a $40 cap threshold.
The GCP billing budget for the chitra.art Maps project, configured to publish threshold messages to a Pub/Sub topic rather than email. Email is for humans; this needed a machine.

Four pieces, none of them clever on their own:

  1. A budget on the GCP billing account, scoped to the project, with a Pub/Sub topic as the alert channel instead of email.
  2. A Cloud Function subscribed to that topic, about 80 lines of Python, that fires on every threshold message. It reads costAmount and budgetAmount from the Pub/Sub payload and decides whether to act.
  3. When costAmount exceeds the cap, the function calls the Cloud Billing API and detaches the billing account from the project. Not "throttle," not "alert" — detach. Resources stop accepting new requests within minutes because the project is no longer attached to a billable account.
  4. A daily Cloud Scheduler job re-checks the day's spend and re-detaches if something has slipped back into a billable state due to a manual change.

The detach-the-billing-account move is the actual trick. Stopping individual services is a game of whack-a-mole; killing the billing account is one API call and it covers every service in the project, including ones I haven't enabled yet but might.

Cloud Function logs showing a budget threshold message being received and the billing-account detach API call returning success.
A dry-run of the killswitch from a synthetic Pub/Sub message. In production the same log line ends with billingInfo.billingEnabled: false.

I keep the actual cap conservative — somewhere around $40/month for the Maps surface — because the cost of being wrong is "Google Maps stops rendering on /communities/[artist] for a few hours until I see the alert," and the cost of not being wrong is reading about myself on Hacker News.

What this is and what it isn't.

It's a poor person's hard cap. The detach is reversible — I can re-attach the billing account in two clicks — but it does interrupt service while it's detached. That's the same tradeoff Azure made when it shipped the real version: cap-triggered shutdown is a deliberate choice to prefer "down" over "billed into oblivion." For a side project where the alternative is staring at a five-figure email, I'll take down.

It is not what I'd run on a production system that takes payments. For chitra.art's API surface, which lives on Azure Container Apps, I rely on Azure's actual budget controls and Cost Management alerts, because that infrastructure is the part where "down" is worse than "expensive." The Google project is a single-purpose island for a single SDK with a public key. Different threat model, different control.

What would change my mind.

I'd rip out the killswitch if Google shipped a real one — and they could, tomorrow. A toggle on the budget object that says "if cost > budget, disable billing-account link" would do it. Three lines of UI and they already own the API. I'm not holding my breath.

I'd also rip it out if the cost surface stopped being a public key. The day chitra.art proxies Maps tile requests through our own backend (we've talked about it), the public key goes away and the killswitch becomes unnecessary.

Until one of those things happens, the rule is simple: anything you ship with a public Google API key on the internet is one motivated abuser away from a teaching moment. Build the killswitch first. The email alerts are for after the killswitch has already done its job.