One product, two clouds is not a fashion statement. It is what you get when teams inherit workloads, vendor strengths differ, and shipping beats a pure architecture diagram.
The problem
Everything on one VM is simple until deploys fight each other, scaling is coarse, and a batch job starves the API. Spreading services without a clear placement rule creates the opposite mess: mystery latency and duplicated secrets.
Placement rules that stuck
| Workload | Landed on | Why | |----------|-----------|-----| | Stateless HTTP APIs | Cloud Run | Scale to demand, clean revisions | | Sticky / heavier workers | EC2 | Long processes, fewer cold starts | | Object storage / assets | S3 | Durable, cheap, CDN-friendly | | Short event jobs | Lambda | Burst work without babysitting a box | | Builds / pipelines | Cloud Build | Close to GCP deploy targets |
Firebase showed up where mobile auth and realtime-ish client needs were faster than reinventing them.
Failure modes
Cross-cloud calls without budgets become silent latency. We treated region and auth boundaries as first-class: shared secrets in one place, explicit service accounts, and timeouts that fail loudly.
Lambda timeouts looked like "flaky API" until we logged invocation ids end-to-end. Cloud Run cold starts surprised us on chatty admin tools — solved with min instances only where cost justified it.
What I would repeat
- One placement table in the repo README so new services do not invent a third pattern
- Health checks and deploy rollback as boring defaults
- Cost alerts early — cloud sprawl is a product bug
Takeaway
Multi-cloud works when each platform has a job. It fails when every service can live anywhere and nowhere is documented.