Microservices Architecture in GCP: When to Use Cloud Run, GKE, and Pub/Sub

Microservices means splitting one application into smaller, independently deployable services that communicate over a network. The main benefit is that separate teams can build, test, and deploy their service without coordinating with everyone else. The main cost is that every function call that becomes a network call can now fail, time out, or slow down, and debugging spans multiple systems instead of one. Most small teams should start with a monolith or a modular monolith and only extract services when they have a concrete reason. This page helps you decide when microservices are worth the trade-off in GCP, which services to use, and how to avoid the most common mistakes.

Simple explanation

Think of a monolith like a single restaurant where one kitchen handles everything: appetisers, mains, desserts, and drinks. The whole team works in one room, shares the same fridge, and coordinates face to face. It works great when the restaurant is small.

Microservices is like splitting that restaurant into separate food stalls in a food court. Each stall has its own kitchen, its own menu, and its own staff. The appetiser stall can upgrade its oven without shutting down the dessert stall. If the drinks stall breaks down, people can still order food. But now the stalls need to communicate through order slips, and someone needs to make sure the slips actually arrive. That communication layer is the new complexity.

In software terms: your one application becomes several smaller services (an accounts service, a product service, an order service, a notification service). Each runs in its own container, owns its own database, and deploys on its own schedule. Teams can work independently. Services scale independently. But instead of one deployment pipeline, you have four. Instead of calling a function, your order service sends an HTTP request to the product service, and that request can fail, return slowly, or return an error your code was not expecting. For a small team, this overhead often costs more than it saves.

When microservices help and when they do not

Microservices solve coordination problems at scale. They add operational problems everywhere. Before splitting anything, ask whether the thing slowing you down is actually the monolith or something else entirely, like slow tests, unclear ownership, or missing CI/CD.

When microservices make sense
  • Multiple teams (roughly 8+ engineers) need to deploy independently, and a shared release cycle is the bottleneck.
  • Different parts of the application have genuinely different scaling needs. A checkout service needs 10x capacity during sales events while an admin dashboard idles.
  • Parts of the system have different technology requirements. A Python ML model next to a Go API next to a Node.js frontend.
  • You need fault isolation. A failure in one area must not bring down unrelated features.
  • Your domain boundaries are well understood and stable. You know where the seams are.
When a monolith or modular monolith is better
  • Your team is small (fewer than 8 engineers who can coordinate releases easily).
  • You are still discovering the right domain boundaries. Wrong service boundaries are extremely expensive to fix once services are deployed and other teams depend on their APIs.
  • The operational overhead of multiple deployment pipelines, distributed tracing, and inter-service auth would slow you down more than the coupling does.
  • Your application is latency-sensitive and the added network hops would hurt the user experience.
  • You do not yet have reliable observability. Without distributed tracing and centralised logging, debugging microservices is guesswork.
The modular monolith: the underrated middle ground

A modular monolith is a single deployed application with clearly defined internal modules and strict dependency rules between them. Think of it as drawing walls inside one building instead of constructing separate buildings. You get clean architecture and independent testability without network complexity. It is much easier to extract a well-defined module into a microservice later than to fix a poorly bounded one. Start here.

Microservices vs monolith vs modular monolith

This table compares the three main approaches across the factors that matter most when choosing an architecture.

FactorMonolithModular monolithMicroservices
Team size1-8 engineers3-15 engineers10+ engineers, multiple teams
Deployment speedOne pipeline, fast to set upOne pipeline, fast to set upPer-service pipelines: slower to set up, faster per team at scale
Operational overheadLow (one service to monitor)Low (one service, cleaner internals)High (many services, many pipelines, many things to monitor)
Scaling flexibilityScale the whole applicationScale the whole applicationScale each service independently
Debugging complexityLow (one process, one log stream)Low to medium (one process, module boundaries help)High (distributed tracing and correlated logs across services)
Fault isolationLow. One bug can crash everythingMedium. Modules limit blast radius inside the processHigh. A crashed service does not take down unrelated ones
Best fitEarly-stage products, small teams, rapid prototypingGrowing teams that want clean boundaries without network complexityLarge organisations with multiple teams that need independent deployability

If this table does not make the choice obvious, default to the modular monolith. You get clean separation of concerns, testable modules, and an easier path to extraction when (and only when) a module genuinely needs to become its own service.

How microservices work in GCP

Theory is easier to understand with a concrete example. Here is how a realistic ecommerce application flows through a microservices architecture on GCP.

Architecture overview

The application has five services: an API gateway, a product catalogue, an order service, a payment service, and a notification service. Here is how a customer placing an order flows through the system:

Customer (browser/mobile)
|
v
Cloud Load Balancer
|
v
API Gateway (Cloud Run)     authenticates request, routes to backend
|
|— GET /products —> Product Service (Cloud Run)     reads from Firestore
|
|— POST /orders —> Order Service (Cloud Run)        writes to Cloud SQL
|
|— sync call —> Payment Service (Cloud Run)
|                     returns success/failure
|
|— publishes “order-created” event to Pub/Sub
|
|— Notification Service (Cloud Run)
|       sends confirmation email
|
|— Analytics pipeline (Dataflow)
writes to BigQuery

What each GCP service does in this flow

  • Cloud Run hosts each service as an independent container. Each scales based on its own traffic. The product catalogue scales up during browsing while the payment service scales during checkout.
  • Pub/Sub carries the “order-created” event. The order service publishes once. The notification service and analytics pipeline each subscribe independently and neither knows about the other.
  • IAM controls which service can call which. The API gateway’s service account has the roles/run.invoker role on the order and product services. The payment service’s service account has no access to the product service because it does not need it.
  • Cloud Trace follows a single customer request across the API gateway, order service, and payment service so you can see exactly where time is spent.
  • Cloud Monitoring tracks latency, error rate, and throughput per service. Alerts fire if the payment service error rate exceeds a threshold.
Two paths, two strategies

The synchronous path (API gateway to order service to payment service) is the critical user-facing path where latency directly affects the customer experience. The asynchronous path (Pub/Sub to notification and analytics) is decoupled. If the notification service is temporarily down, the order still succeeds and the email sends once the service recovers. Designing with both paths in mind is what makes microservices resilient.

Choosing GCP services for microservices

Cloud Run

Cloud Run is the default choice for stateless microservices in GCP. Each service is a separate Cloud Run deployment with its own URL, its own scaling configuration, and its own service account. You pay only for CPU and memory used during request processing, so an idle service costs nothing.

Cloud Run works best when your services are stateless, handle HTTP or gRPC requests, and do not need persistent local storage. Most microservices fit this model. For a detailed comparison with other compute options, see choosing between Cloud Run, GKE, and Compute Engine.

GKE

GKE makes sense when you have many services with complex networking requirements, need a service mesh for mutual TLS, run stateful workloads alongside stateless ones, or have workloads that do not fit Cloud Run’s request-based execution model. Use GKE Autopilot to eliminate node management and focus on workload configuration.

GKE gives you more control at the cost of more operational overhead. You are managing a cluster, not just deploying containers. For a direct comparison, see GKE vs Cloud Run.

Pub/Sub

Pub/Sub is GCP’s fully managed messaging service and the backbone of asynchronous communication between microservices. A service publishes a message to a topic. One or more subscriptions deliver that message to consuming services. Publishers and subscribers are completely decoupled: neither needs to know the other exists.

Pub/Sub handles message durability, delivery retries, and backpressure. Choose between push and pull delivery depending on whether your subscriber is always running (pull) or event-triggered like Cloud Run (push).

API management

When microservices need to be exposed to external consumers (mobile apps, partner integrations, or third-party developers), add an API management layer. Apigee provides authentication, rate limiting, API versioning, and developer portal capabilities without changing your service code. For simpler internal-to-external gateway needs, a Cloud Run service acting as a reverse proxy or Cloud Load Balancing with serverless network endpoint groups can be sufficient.

Quick decision shortcut

Think of Cloud Run like hailing a taxi: you get a ride exactly when you need one, pay only for the trip, and never worry about parking or maintenance. GKE is like owning a fleet of cars: more control, more options, but you are responsible for fuel, insurance, and garage space even when the cars are parked. Start with the taxi. Buy the fleet when the taxi no longer fits your needs.

Synchronous vs asynchronous communication

How services talk to each other is one of the most important design decisions in a microservices architecture. Think of synchronous calls like a phone conversation (you wait for a reply before you can act) and asynchronous calls like a text message (you send it and get on with your day). The wrong default creates cascading failures or unnecessary complexity.

Synchronous: when you need an answer now

Use synchronous HTTP or gRPC when the calling service genuinely cannot continue without the response. A checkout page that needs to display a calculated price before the user clicks “Pay” has no choice. That call is synchronous.

For Cloud Run to Cloud Run calls, authenticate using service account identity tokens via IAM:

# Call another Cloud Run service with IAM authentication
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
  https://pricing-service-abc123-uc.a.run.app/price?product=p-991

In application code, use the Google Auth libraries to generate identity tokens dynamically. Never store credentials for service-to-service calls. Cloud Run services authenticate using their own service account identity.

Asynchronous: when you do not need to wait

Use Pub/Sub when the caller can continue without waiting for a response, when multiple services need to react to the same event, or when you want to absorb traffic spikes without overwhelming downstream services. After a user places an order, the order service publishes an event and returns HTTP 200 immediately. The notification, fulfilment, and analytics services consume the event at their own pace.

# Publish an event to Pub/Sub
gcloud pubsub topics publish order-events \
  --message='{"order_id": "ord-8821", "user_id": "u-4432", "total": 49.99}' \
  --attribute=event_type=order-created \
  --project=my-app-prod

For the full pattern, see event-driven systems in GCP.

Decision table

SituationUse synchronous (HTTP/gRPC)Use asynchronous (Pub/Sub)
Caller needs data to continueYesNo
Multiple consumers for the same eventNoYes
Downstream service can be temporarily unavailableNo, the caller blocks or failsYes, messages queue until recovery
Traffic is spiky and downstream cannot absorb burstsNo, downstream gets overwhelmedYes, Pub/Sub absorbs the spike
Strict ordering mattersEasier to guaranteePossible with ordering keys, but adds complexity
Latency budget is very tightYes, one fewer hopNo, adds publish and deliver latency
Rule of thumb

Default to asynchronous where the design allows it. A downstream service being temporarily unavailable does not block the upstream service, and messages queue in Pub/Sub until the subscriber recovers. If you are unsure whether a call should be sync or async, ask: “Does the user need to see the result of this action right now?” If yes, synchronous. If no, asynchronous.

Service identity and authorisation in GCP

In a microservices architecture, services call other services. You need to control which services are allowed to call which. Think of it like an office building with keycard access: each employee (service) gets their own badge (service account), and each room (other service) only opens for authorised badges. In GCP, the answer is service accounts and IAM. Not API keys, not shared secrets, not JWTs you generate yourself.

How it works: Each service runs as a dedicated service account. To allow Service A to call Service B on Cloud Run, grant Service A’s service account the roles/run.invoker role on Service B. No other service can call B unless explicitly granted that role.

# Grant the order service permission to call the payment service
gcloud run services add-iam-policy-binding payment-service \
  --region=us-central1 \
  --member="serviceAccount:order-svc@my-app-prod.iam.gserviceaccount.com" \
  --role="roles/run.invoker" \
  --project=my-app-prod

This keeps service-to-service authentication inside GCP’s IAM system with no credentials to rotate and no secrets to leak. See the least privilege guide for how to scope service account permissions correctly.

What not to do
  • API keys for service-to-service auth. API keys identify a project, not a service. They cannot be scoped to specific services and are easy to leak.
  • Shared secrets passed in headers. Secrets need rotation, and rotation across multiple services is error-prone and easy to get wrong.
  • Overly broad IAM roles. An order service does not need roles/editor on the project just to call one other service. Grant the narrowest role that works.

Building resilience into service calls

Network calls fail. In a monolith, a function call either works or throws an exception immediately. In microservices, calls can hang, return slowly, or fail in ways your code did not anticipate. Design for failure from the start.

Timeouts

Always set an explicit timeout on every outbound HTTP call. Without a timeout, a hanging downstream service holds your connection open indefinitely. Your service’s threads or connection pool fills up. Your service stops responding to new requests. The failure cascades upstream. A 2 to 5 second timeout is typical for synchronous service-to-service calls.

Retries with exponential backoff

Retry transient failures (network blips, 503 responses) with increasing delays: wait 1 second, then 2, then 4. Add jitter (a small random delay) so that many clients retrying at the same time do not all hit the recovering service simultaneously. Do not retry non-transient errors like 400 or 404. Those will fail every time.

Circuit breakers

A circuit breaker works like the fuse box in your house. When too much current flows through a circuit, the fuse trips and cuts power immediately rather than letting wires overheat and start a fire. In software, if a downstream service is consistently failing, the circuit breaker “opens” and your service stops calling it entirely. Calls fail immediately without waiting for a timeout. Periodically, the breaker lets one call through to test if the downstream service has recovered. If it succeeds, the breaker closes and normal traffic resumes.

How a failure cascade works

Picture three services in a chain: A calls B, and B calls C. Service C’s database slows down, so C starts responding in 30 seconds instead of 200 milliseconds. B waits 30 seconds for each call to C, so B’s connection pool fills up. B stops responding to A. A’s connection pool fills up. A stops responding to users. One slow database brought down three services, not because they crashed but because nobody set a timeout.

A 3-second timeout on B’s calls to C would have contained the damage. B would have returned errors for C-dependent requests but stayed healthy for everything else. This is the most common production failure mode in microservices systems.

Observability for microservices

In a monolith, you read one log file. In a microservices system, a single user request can touch five services. When something fails, you need to find the failing service, the failing request, and the root cause quickly. This requires three things set up before you go to production, not after.

Distributed tracing

A trace follows a single request across every service it touches, like a tracking number for a package that passes through multiple sorting facilities. Use Cloud Trace and propagate the X-Cloud-Trace-Context header on every outbound call. Cloud Trace shows a waterfall view of each service the request passed through, with timing for each span. This tells you exactly where time is spent and where failures occur. For a deeper explanation, see the distributed tracing guide.

Structured logging

Log in JSON format and include the trace ID in every log entry. This lets you filter logs across all services for a single request in Logs Explorer. Without a trace ID in the logs, correlating a payment failure to the order request that triggered it means searching by timestamp and guessing.

Think of it this way

A trace ID is like a case number at a hospital. When a patient visits three departments, every department logs notes under the same case number. Without it, the cardiologist’s notes, the lab results, and the pharmacy records are just three unconnected documents. With it, the full picture is one search away.

Metrics and alerting

Track three metrics per service at minimum: request rate, error rate, and latency (p50, p95, p99). Use Cloud Monitoring to create alerts that fire when error rates spike or latency degrades. Cloud Run and GKE both export these metrics automatically.

Start on day one

Add tracing and structured logging from the very first service. Retrofitting trace propagation into an existing system means updating every service. If you have 15 services when you realise you need tracing, that is 15 services to modify and redeploy. The cost of adding it later grows linearly with the number of services you have.

When to use this architecture

Use this checklist to evaluate whether microservices are the right move for your situation. The more items you check, the stronger the case. If you check fewer than three, a modular monolith is likely the better starting point.

  • Multiple teams need independent deploy cycles. Coordinating releases across teams is the bottleneck, not the code itself.
  • Parts of the application have very different scaling profiles. One service needs 50 instances during peak traffic while another handles 2 requests per minute.
  • Domain boundaries are well understood. You can clearly define which service owns which data and which business logic. Boundaries are not expected to change soon.
  • Fault isolation is a hard requirement. A failure in payments must not take down product browsing.
  • Different services have different technology requirements. A Python ML model alongside a Go API alongside a Node.js frontend.
  • Your team has the operational maturity. You already have CI/CD, monitoring, alerting, and incident response processes. Adding microservices without these is adding complexity onto a weak foundation.
  • You are willing to invest in observability upfront. Distributed tracing, structured logging, and per-service dashboards are not optional. They are prerequisites.
  • Your services can be stateless. Stateless services are easier to scale, deploy, and replace. Stateful services in a microservices architecture add significant complexity.
The extraction rule of thumb

If you are not sure whether to extract a service, do not. Wait until the pain of keeping it in the monolith is concrete and measurable: deployment conflicts, scaling bottlenecks, or team coordination issues. Premature extraction creates distributed complexity for a problem you do not have yet.

Common mistakes

  1. Splitting services before understanding domain boundaries. Service boundaries should reflect your organisation’s team structure and the application’s natural domain boundaries. Splitting by technical layer (a “data layer service” and a “logic layer service”) creates services that are tightly coupled in disguise. Every domain change requires coordinated deployments across both. Get the boundaries right inside a monolith first.

  2. Sharing a database between services. If two services read and write the same tables, they are coupled at the data layer. A schema migration by Service A breaks Service B. Each service must own its data. If another service needs that data, expose it through an API or a Pub/Sub event.

  3. Not setting timeouts on outbound calls. A hanging downstream service without a timeout fills your connection pool, makes your service unavailable, and cascades the failure to your callers. This is the most common production failure mode in microservices systems. Always set timeouts.

  4. Adding distributed tracing after the fact. Trace propagation requires changes to every service. If you have 20 services before adding it, that is 20 services to update and redeploy. Add the Cloud Trace client library and propagate the trace context header from day one.

  5. Over-splitting too early. Starting with 15 microservices for a new product built by a team of four creates enormous operational overhead for zero benefit. You spend more time managing infrastructure, pipelines, and inter-service communication than building features. Start with a monolith or modular monolith, and extract services one at a time when a specific, concrete problem demands it.

  6. Weak service boundaries that require coordinated deploys. If deploying Service A always requires deploying Service B at the same time, they are not independent services. They are a distributed monolith with extra network overhead. True microservices can be deployed independently without breaking each other’s contracts.

The distributed monolith trap

The worst outcome is not staying with a monolith. It is building microservices that cannot actually be deployed independently. You end up with all the operational overhead of distributed systems (network failures, tracing, per-service pipelines) combined with all the coordination overhead of a monolith (lock-step releases, shared schemas). If your “microservices” always ship together, step back and reconsider the boundaries before adding more.

Frequently asked questions

Should I start with microservices or a monolith?

Start with a monolith or modular monolith. A monolith is simpler to build, test, deploy, and debug for any team under about ten engineers. Microservices add network latency, distributed tracing requirements, and per-service deployment pipelines that only pay off once coordinating releases across teams becomes the bottleneck. Build clean module boundaries inside the monolith first. When a specific module genuinely needs independent scaling or a separate deploy cycle, extract it.

Cloud Run or GKE for microservices?

Cloud Run is the default for stateless HTTP and event-driven services. It scales to zero, requires no cluster management, and keeps per-service costs low when traffic is uneven. Choose GKE when you need a service mesh, long-running background processes, stateful workloads, or fine-grained networking between many services. GKE Autopilot reduces the operational burden if you do go the Kubernetes route.

Should microservices share a database?

No. A shared database couples services at the data layer. A schema change in one service can break another. Each service should own its own data store. If services need data from each other, expose it through APIs or events rather than shared tables. This is one of the hardest boundaries to enforce and one of the most important.

When should I use Pub/Sub instead of HTTP?

Use Pub/Sub when the caller does not need an immediate response, when multiple services need to react to the same event, or when you want to protect downstream services from traffic spikes. Use HTTP or gRPC when the calling service cannot continue without a response, for example a checkout page that must display a calculated price before the user can proceed.

Do I need a service mesh?

Probably not yet. A service mesh like Istio adds mutual TLS, traffic splitting, retries, and observability at the infrastructure layer. That is valuable when you have dozens of services and configuring each one individually becomes unsustainable. For fewer than ten services, handle TLS with IAM-based auth on Cloud Run and implement retries in application code. Revisit the decision when per-service configuration becomes a bottleneck.

Last verified: 26 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.