Updated recently
Last updated:
Quick Answer: The GCP Cloud Run Cloud Functions question is really about layers, not alternatives. Cloud Run runs containers over HTTP and is the default for most services you build. Cloud Functions runs short event-driven code with no container to think about, and its second generation runs on Cloud Run infrastructure anyway. Pub/Sub is the message backbone between them — at-least-once delivery, retries and dead-letter topics. The patterns that matter are fan-out, retry with a DLQ, and idempotent consumers.
Three Building Blocks, Not Three Competing Options
The most common beginner confusion is treating these as alternatives. They operate at different layers. Cloud Run is where your request-handling application runs: you give it a container, it gives you a URL, scales to zero when idle, and scales out under load. Cloud Functions is where a small piece of event-driven logic runs: no container, no server, triggered by an event rather than a request. Pub/Sub is neither — it is the durable queue and fan-out system that delivers events to either of them.
That composition is the actual design pattern. A user uploads a file to Cloud Storage, Pub/Sub publishes an object-created event, a Cloud Function or a Cloud Run worker consumes it and does the processing, and results land in BigQuery. Each piece does one job, and none of them needs to be reachable from the internet for the others to work with it.
The GCP Cloud Run Cloud Functions Decision, and Where Pub/Sub Fits
| Factor | Cloud Run | Cloud Functions (2nd gen) | Pub/Sub |
|---|---|---|---|
| What it runs | Your container, any language | Your function code | Nothing — it delivers messages |
| Trigger | HTTP, Pub/Sub, Eventarc events | Events: Pub/Sub, Storage, Firestore, Scheduler | Produced by publishers |
| Scaling | Per-instance concurrency, scales to zero | Instance-based, configurable max | Automatic by subscription throughput |
| Long-running work | Yes — request timeout up to 60 minutes | Limited — keep functions short | Yes, but it is transport only |
| Cold starts | Present, usually small; mitigate with min instances | More noticeable at low traffic | Not applicable |
| Control | Container, ports, concurrency, filesystem | Minimal — code and configuration | Ack deadlines, retries, ordering keys |
| Cost model | Per request + vCPU-seconds while handling | Per invocation + instance time | Per GB of message traffic |
| Best for | APIs, web apps, containerised services | Glue, one-shot event handlers | Decoupling, fan-out, buffering |
Cloud Run: The Default Choice, and the Lever You Must Understand
Cloud Run’s defining number is concurrency — how many simultaneous requests one instance handles. The default is generous (80, with each request served on its own thread in a language runtime that supports threading). That single value determines your cost more than anything else: a Node or Python API handling 80 concurrent requests per instance needs far fewer instances than one configured with concurrency 1, and teams that never touch it routinely pay multiples of what they should.
Choose it when you have an HTTP service, a container, or anything longer than a few seconds. Set a minimum instance count for anything latency-sensitive — scale-to-zero is wonderful for cost and terrible for the first user’s cold start after a quiet night. Choose between CPU always allocated and CPU only during request processing based on whether you do background work after the response; getting this wrong is a classic cause of truncated async work.
Cloud Functions: Where It Fits, and Where It Stops
Cloud Functions is at its best for glue: reacting to an object upload, calling a webhook, refreshing a cache, emitting an analytics event. You write a single handler, point it at a trigger, and you are deployed — no Dockerfile, no health checks, no rollout strategy for a first version.
Its limits are real and worth knowing before you commit. Functions are billed and scheduled per invocation instance, so the economics break down for long-running or high-concurrency work where one container serving many requests would be cheaper. The first generation’s tight timeout and single-concurrency defaults caused a decade of awkward workarounds; second generation runs on Cloud Run infrastructure with far higher limits, which is where you should be starting now. And functions invite a design smell: if your handler grows a queue, a retry policy and a state file, it has quietly become a service — move it to Cloud Run.
Pub/Sub: The Part That quietly Decides Whether This Works
Pub/Sub gives you at-least-once delivery. Read that again before building on it: messages can and will be delivered more than once, after ack timeouts, redeliveries and transient failures. The platform guarantees delivery attempts; it cannot guarantee your handler runs exactly once. Idempotency is your responsibility, always.
The four settings that matter in production:
- Ack deadline — how long a consumer has before an unacknowledged message is redelivered. Set it to your real processing time, not the default.
- Retry policy with a dead-letter topic — after a bounded number of failures, park the message somewhere you can inspect instead of looping forever.
- Flow control — bound outstanding messages so a slow consumer does not drown its own instance.
- Ordering keys — only when you genuinely need per-key ordering, because they constrain throughput and are not free of complexity.
# Container service — concurrency is the cost lever
gcloud run deploy orders-api --image gcr.io/demo-42/orders-api:1.0
--region asia-south1 --concurrency 80 --min-instances 1
--memory 512Mi --cpu 1 --no-allow-unauthenticated
# Event-driven glue on top of a queue
gcloud functions deploy process-upload
--gen2 --region asia-south1 --runtime python312
--trigger-topic uploads --retry --timeout 300s
--max-instances 20
# Durable queue with a dead-letter path — do this on day one, not after the incident
gcloud pubsub topics create uploads-dlq
gcloud pubsub subscriptions create uploads-sub --topic uploads
--ack-deadline 60
--retry-policy=minimum-backoff=10s,maximum-backoff=600s
--dead-letter-topic=uploads-dlq --max-delivery-attempts=5
A Double-Charge Incident, Explained
The most common production failure in this stack is not an outage — it is duplication. A company processed payment-confirmation events with a Cloud Function triggered from Pub/Sub. The handler charged the customer and then wrote a record to Firestore. During a deploy, instance startup latency pushed several messages past their ack deadline; Pub/Sub, correctly doing its job, redelivered them. The handler charged again. Forty-one customers were double-charged before the alert fired, and the reconciliation took two days.
Nothing in the platform was broken. The consumer was not idempotent, there was no deduplication key, and there was no dead-letter topic — so the same failing messages were retried indefinitely instead of surfacing for a human to see. The fix was small and structural: a deterministic event ID stored with a twenty-four-hour TTL checked before any charge, an ack deadline matched to actual processing time, a DLQ with alerting on depth, and a metric on redelivery rate that would have shown the problem within minutes of the deploy. Our broader point applies here — asynchronous design is where juniors most often think they are finished when they are halfway.
How to Choose Between Cloud Run and Cloud Functions
- Is it an HTTP service a user or system calls? Cloud Run, always.
- Is it a reaction to an event with no incoming HTTP? Cloud Functions for short glue; Cloud Run Jobs for anything over a minute or with heavy dependencies.
- Does it need more than one concern — a queue, state, retries, background work? Cloud Run. That is a service, not a function.
- Is cost at high volume the dominant concern? Cloud Run with tuned concurrency usually wins, because one instance absorbs many requests instead of each invocation spinning up an instance.
- Either way, put Pub/Sub in front of anything that must not lose data and make the consumer idempotent. That decision is independent of compute choice and it is the one that saves you at 3 AM.
For where these skills land in the market, the platform pay bands are in our Google Cloud engineer salary guide, and the streaming-to-analytics path these patterns usually feed — Dataflow and BigQuery — is mapped in the warehouse comparison. If you are still weighing GCP against AWS for your career, read AWS vs GCP in India first.
Training Resources
Build the full event-driven path in SkilBrill Google Cloud Development Training — Cloud Run services, Cloud Functions, Pub/Sub retry and dead-letter design, and the idempotency patterns production requires.
FAQ
What is the difference between Cloud Run and Cloud Functions in GCP?
Cloud Run runs your container over HTTP with tunable concurrency and long timeouts; Cloud Functions runs short code in response to events with no container to manage. Second-generation Cloud Functions runs on Cloud Run infrastructure, so the real distinction is request-handling service versus event glue.
When should I use Cloud Functions instead of Cloud Run?
For short, single-purpose event handlers — object created, row inserted, cron tick — where a container would be overhead. If the handler starts managing retries, state or background work, move it to Cloud Run.
How does Pub/Sub guarantee message delivery?
At-least-once: messages are redelivered until acknowledged, after a retry policy and bounded delivery attempts, then routed to a dead-letter topic if configured. Exactly-once processing is not provided — your consumer must be idempotent.
Why is my Cloud Run service slow on the first request?
Cold start — the instance was scaled to zero. Set a minimum instance count for latency-sensitive services, keep the container image lean, and check that startup work such as large dependency loads or remote configuration fetches is not happening at request time.
Is serverless on GCP cheaper than a VM?
For spiky or low traffic, yes — scale-to-zero means you stop paying when idle. For steady 24×7 load on a single service, a right-sized Compute Engine VM or a reserved instance is frequently cheaper. Model it against real traffic rather than assuming either is always the answer.
