Engineering · Cloud costs
We Paid for 106 Cloud Run Instance-Hours a Day Because of a Feature That Fires Once Every Two Days
On 2026-09-22 the Cloud Run compute line for our backend was almost the whole bill: 60 to 106 billable instance-hours a day for a service that handled about 0.5 requests per second. The cause was a live-reply feature built on server-sent events, made expensive by session affinity and always-on CPU.
What we saw
I build Bonstructer, a receipt notebook app, and run the backend as a single Cloud Run service. On 2026-09-22 I looked at the bill and the compute line was almost entirely that service. Billable instance time had gone from 1 hour a day on 09-09 to somewhere between 60 and 106 hours a day. That came to roughly $10 to $18 a day.
The traffic did not explain it. The service handled about 0.5 requests per second. At that volume an instance should be idle most of the day, and idle instances on Cloud Run cost close to nothing.
There was no database or Kubernetes cluster to blame. Firestore, model calls, Cloud Build, Artifact Registry and logging were all minor. It was Cloud Run compute, and only that.
Why: three causes that multiplied each other
1. Server-sent events that never ended
The app has a feedback feature: you send an idea or a problem report, and a reply shows up in the app. To make replies appear live I had added a server-sent events endpoint. Every signed-in Android app, web tab and admin tab opened a stream, held it for the full 540 second request timeout, and reconnected within 2 seconds.
That is how Cloud Run bills: an open request is an active instance. It does not matter that nothing is being sent on the stream. While a request is in flight, the instance counts as busy and you pay for it. I counted about 27 streams that were open permanently, so no instance ever got to scale to zero. The feature itself is rare: 7 replies in 14 days on my own account.
2. Session affinity kept old revisions alive
The service was deployed with session affinity. Affinity pins a client to the revision it first connected to. Combined with clients that reconnect every few minutes, this meant old revisions kept receiving traffic after I had moved traffic away. Revision 00621 still took 185 to 510 requests per 30 minutes, 2.5 hours after a deploy. Five revisions held about 20 instances between them. I deploy often (9 to 83 times a day on busy days), so every deploy left another revision pinned.
3. CPU always allocated
The service ran with CPU always allocated, which bills every instance-second at the full 2 vCPU, not only while a request is being handled. The median CPU use was 1 percent. So each of those pinned instances was billed at full size for doing almost nothing.
The fix
I removed the streaming from the clients instead of tuning it.
- The existing list endpoints for feedback now return a weak ETag and answer 304 to a matching If-None-Match. The body did not change.
- The web app polls every 120 seconds while the tab is visible, every 45 seconds on the Ideas page, and immediately on focus. Nothing runs in a hidden tab. The admin web polls every 60 seconds while visible.
- Android does the same: 120 seconds in the foreground, 45 with the feedback page open, a check on resume, never in the background.
- The deploy script and both Cloud Build configs now state CPU throttling and no session affinity explicitly, so a default cannot bring them back.
- The streaming route stays for the closed-testing Android build that still uses it, and browsers get a 410 on it.
A reply now appears up to two minutes later instead of instantly. For a feature that fires every two days I accepted that.
After numbers
At 07:31 UTC on 2026-09-23, once the browsers got the 410, billable time was 0.3 instance-hours a day. The night before it was still 48 hours, because old browser bundles kept streaming until someone reloaded them. Firestore reads sit around 13k an hour overnight, with the feedback polls about half of that, well under $1 a day.
One leftover: older Android builds (0.1.245 to 0.1.273) still held streams open and added 4 to 5 instance-hours a day on 09-23. I could not refuse them from the server, because they retry every 2 seconds on any error and reconnect instantly on a clean close. The fix was to promote the closed-testing build to one that only polls, then retire the route for everyone once logs show no more streams. I do not have an after-fix dollar figure per day in my notes, only the instance-hours.
Checklist before you add streaming to Cloud Run
Read the change against each of these before you write it.
- Does it keep a request open or an instance alive? SSE, streaming, long polling, WebSockets, keep-alive pings, min-instances above 0, no CPU throttling, session affinity, a scheduler more often than hourly, timers that expect CPU between requests. One 2 vCPU instance kept busy around the clock is about $4 a day.
- Does it add recurring traffic per client? Polling cost is cadence times clients times cost per call. Poll only while visible, use ETag and 304, never in the background.
- Does it fan out reads or writes? A whole-collection Firestore get in a poll, a listener that re-fetches, per-keystroke calls, N+1 lookups.
- Does it add paid model calls? New call sites, loops or retries around one, bigger prompts, a switch to a pricier model.
- Does it touch build and storage? Build minutes, image growth, log volume, data that is never purged.
- Does it raise deploy frequency? Every revision cold-starts with CPU boost, so batch small deploys.
Then write the expected cost effect down in one or two sentences before you ship, and look at billable_instance_time within an hour after. We now keep that as a rule for every change, and the target is low single digits of instance-hours a day.
If you run streaming on Cloud Run with a mostly idle service, I would like to know how you keep it from dominating the bill.
I'm the developer of Bonstructer, a receipt notebook app: bonstructer.com.
Sources
- Google Cloud: Cloud Run pricing (accessed October 2026)
FAQ
Does an idle server-sent events stream cost money on Cloud Run?
Yes. An open request keeps an instance active, and you are billed for it even when nothing is sent on the stream. About 27 permanently open streams meant no instance ever scaled to zero.
What replaced the streaming in Bonstructer?
Polling with a weak ETag and 304 answers: every 120 seconds while the tab is visible, every 45 seconds on the Ideas page, and never in the background.
What is a good target for Cloud Run instance-hours on a mostly idle service?
Low single digits of instance-hours a day. One 2 vCPU instance kept busy around the clock is about $4 a day.