Cloud architecture

How I Slashed a GCP Bill by 80%: The Hidden Trap of the Serverless CPU Toggle

I dropped my cloud bill by 80% without rewriting anything in Rust. The culprit was a 30-second config fix that silently opted me out of the serverless pricing model entirely - and the fix was a proper event-driven worker architecture.

Author

Published

Updated

Length

12 min · 1,900 words

Key takeaways

  • The --no-cpu-throttling (CPU always allocated) flag on Cloud Run opts you silently into 24/7 always-on billing, destroying the serverless cost model.
  • Cloud Run bills for active CPU milliseconds. With throttling off, containers log 168 billable instance-hours per week even when idle.
  • Google Cloud Tasks is purpose-built for this problem: explicit HTTP-targeted worker invocations with retries, rate-limiting, and backoff.
  • The fix is architectural: return 200 OK fast, enqueue a Cloud Tasks job, let the CPU scale to zero, fire the worker separately.
  • Re-enabling CPU throttling plus Cloud Tasks cut billable instance-hours by ~80% with better reliability than the always-on approach.

Last month I dropped my cloud computing bill by 80%. I didn't rewrite anything in Rust or migrate away from serverless. The savings came from realising that a 'genius' 30-second configuration fix I had applied weeks earlier was a silent budget assassin running 24 hours a day.

If you are running Next.js, Node, or Go on Google Cloud Run and rely on background tasks, you are likely at risk of the exact same trap.

The problem: scale-to-zero kills background jobs

Serverless platforms like Cloud Run are cost-effective because they scale to zero. You pay only for the milliseconds a container is actively processing a request. The moment your handler fires a response, Cloud Run throttles the container CPU to near zero.

This creates a hard problem for async work. I was building a webhook flow:

  1. Cloud Run API receives a webhook from a third-party service.
  2. The system needs to parse the payload, sync a batch of heavy images to a GCS bucket, and update the database.
  3. I need to return a `200 OK` immediately so the third party does not time out.

The moment the `200 OK` fires, GCP assumes the work is done. CPU is throttled to near zero. The Node.js event loop jobs - image sync, DB writes - stall, freeze, or fail silently. Network connections drop. The work vanishes.

The expensive workaround: --no-cpu-throttling

I hit the docs and found what looked like a magic wand: CPU always allocated (`--no-cpu-throttling`). I flipped the flag in 30 seconds. The background job issue vanished. The webhooks succeeded, the images synced, and I posted on LinkedIn about how easy the fix was.

When I opened the GCP billing dashboard the next month, my Cloud Run instances were logging 168 billable instance-hours per week. The CPU was kept 'allocated' around the clock waiting for background jobs, so I was billed for 24/7 compute. I was paying premium serverless prices to run what was effectively a traditional always-on VM.

Cost impact: throttling on vs. off for a single Cloud Run service
CPU throttling ON (default)CPU throttling OFF (--no-cpu-throttling)
Billing modelPay per active request msAlways-on: pay 24/7
Idle costZeroFull instance-hour rate
Weekly instance-hours (light traffic)~2–4 hrs168 hrs
Background job reliabilityPoor (jobs stall)Good
Relative monthly costBaseline~10–15x baseline

The real fix: event-driven decoupling with Cloud Tasks

To get that 80% reduction I re-architected the flow. The goal: let Cloud Run safely scale to zero the moment a request ends, while guaranteeing that the heavy background jobs actually execute.

Step one: turn CPU throttling back on. Instant 24/7 billing leak plugged.

Step two: migrate async work to Google Cloud Tasks. Cloud Tasks is specifically built for explicit, point-to-point HTTP execution. You target a specific worker endpoint, guarantee delivery, and get fine-grained control over retries, rate-limiting, and execution timing. This is why I chose it over Pub/Sub - Pub/Sub is great for fan-out messaging, but for webhook processing with one worker and controlled retries, Cloud Tasks is the right primitive.

The new, cost-optimised architecture

  1. Ingestion (fast and cheap): The main Cloud Run API receives the webhook. CPU is throttling-enabled.
  2. The handoff: Instead of executing the heavy image sync inline, the API parses the required IDs, builds a JSON payload, and enqueues a Cloud Tasks job targeting the dedicated worker endpoint.
  3. The quick exit: The API returns `200 OK` instantly. The request ends. Cloud Run throttles CPU to zero. Billing stops.
  4. The worker (on-demand compute): Cloud Tasks fires an HTTP request to the dedicated worker endpoint. Cloud Run spins up fresh compute for exactly this task. The worker completes the heavy sync and scales back down.

Results: faster, cheaper, and actually resilient

  • 80% cost reduction. Billable instance-hours plummeted. The platform idles for free between requests.
  • Bulletproof reliability. Cloud Tasks has built-in retries with exponential backoff. If an image sync fails due to a network hiccup or a database lock, Cloud Tasks retries the worker automatically without touching the main API or losing data in a frozen event loop.
  • Zero main-thread blocking. The webhook ingestion API now responds in milliseconds, completely decoupled from processing time.

The lesson

Serverless infrastructure is cheap because it idles for free. The moment you force it to stay awake to paper over an architectural flaw, you are paying always-on rates for always-on compute - and getting none of the resilience benefits of a real worker queue.

If background jobs are stalling your APIs, don't flip the throttling flag. Build a worker, enqueue a task, and let the cloud scale to zero.

The fix is never the flag. The fix is the architecture.

Frequently asked questions

What does the --no-cpu-throttling flag do on Google Cloud Run?

The --no-cpu-throttling flag (also called CPU always allocated) keeps a Cloud Run container's CPU active even after an HTTP request has completed. By default, Cloud Run throttles CPU to near zero once a response is sent, which is what makes serverless cheap. Disabling throttling means you pay for 24/7 compute regardless of actual traffic - effectively turning Cloud Run into an always-on VM billed at serverless prices.

Why do background jobs stall on Cloud Run after returning a 200 OK?

Cloud Run treats the end of an HTTP request as the signal to throttle the container CPU. Any background work still running in the Node.js event loop - image uploads, database writes, webhook fan-outs - is starved of CPU immediately. Network connections drop, promises hang unresolved, and the work disappears silently. This is by design in the serverless model: compute is only allocated during active request handling.

What is the correct architecture for background jobs on Cloud Run?

The correct pattern is event-driven decoupling via Google Cloud Tasks. The primary Cloud Run handler receives the webhook, parses the payload, enqueues a Cloud Tasks job targeting a dedicated worker endpoint, and immediately returns 200 OK. Cloud Run scales the CPU to zero. Cloud Tasks asynchronously fires an HTTP request to the worker endpoint, which Cloud Run spins up fresh compute for. The worker completes the heavy processing and scales back down. You pay only for the milliseconds each container is actively executing.

Why use Cloud Tasks instead of Pub/Sub for background jobs on Cloud Run?

Cloud Tasks is built for explicit, point-to-point HTTP execution. You target a specific endpoint, control the exact delivery time, set per-task retry limits, and get fine-grained rate-limiting. Pub/Sub is optimised for high-throughput fan-out messaging where multiple subscribers need the same event. For webhook processing where you want guaranteed delivery to one worker with controlled retries and backoff, Cloud Tasks is the right tool.

How much does the Cloud Run CPU throttling flag actually cost?

With --no-cpu-throttling enabled on a single Cloud Run service, a container running 24/7 logs approximately 168 billable instance-hours per week. At standard Cloud Run pricing, a single always-on instance can cost 10-15x more than the same workload processed on-demand with CPU throttling enabled. The exact figure depends on memory allocation and region, but the billing model shifts from pay-per-millisecond to always-on VM pricing.
Google Cloud PlatformCloud RunServerlessCloud TasksCost optimisationBackend architectureNode.js

Let's build together. Waiting to connect.

©2025Vishu Pratap · Software Developer