My talk at NDC focused on Multi-Region Active-Active architecture. For the demo, I deployed a platform capable of scaling across N Azure regions. One of the key points I demonstrated was how easily the platform could evolve from two regions to three with only a small Terraform change. Because the architecture had already been designed around regional expansion, adding another region was mostly a matter of configuration rather than redesign.

That demonstration highlighted something I think gets lost in a lot of cloud architecture discussions: you do not need to launch at maximum scale on day one. What matters more is designing systems that can grow into scale when the business actually needs it.

There is a difference between being capable of scaling and continuously paying for scale you are not yet using.

Recently, I decided to scale the platform back down from three regions to a single region. The primary reason was cost efficiency. Running across three regions pushed the daily operating cost above $50 per day, or roughly $1,500 per month. Scaling down to a single region reduced that cost to approximately $12.70 per day, or about $381 per month.

Surprisingly little changed operationally.

The architecture still supports multi-region deployment. Terraform still allows me to expand into additional regions with relatively small changes. The deployment model still works exactly as designed. I did not remove the capability to scale globally. I simply stopped paying for unused regional capacity before there was sufficient traffic or business demand to justify it.

That distinction matters.

Too often, scalability conversations become centered around proving that a platform can scale infinitely rather than asking whether it currently needs to. It is easy to become attached to the idea of “production-grade” infrastructure long before the production workload actually exists. But scalability is not just a technical problem. It is also a financial one.

Understanding the Cost Drivers

Looking at the Azure cost breakdown, Azure Container Apps are currently the largest contributor to the bill. Each regional “stamp” contains three separate container apps:

  • Frontend
  • Backend
  • Background worker

With three regions deployed, those three applications multiplied into nine container apps total. The architecture scaled cleanly, but so did the monthly bill.

This is one of the subtle realities of cloud-native systems. Modern infrastructure platforms make horizontal expansion remarkably easy. Terraform makes provisioning additional regions almost trivial once the patterns are established. But every layer of resiliency, redundancy, and geographic distribution carries a recurring operational cost.

The technical challenge of multi-region architecture is often easier to solve than the economic challenge of sustaining it.

Optimizing the Backend

One of the more interesting optimizations available in Azure Container Apps is the ability to scale workloads all the way down to zero instances when they are idle.

The background worker in my platform is a perfect candidate for this model because it processes asynchronous events rather than handling live client traffic directly. Unlike the frontend or backend APIs, the worker does not need to maintain constant availability to serve interactive requests. Its primary responsibility is reacting to queued work as demand arrives.

That changes the scaling strategy entirely.

Instead of paying to keep worker instances continuously running, I can allow the service to scale dynamically based on queue depth. During periods of inactivity, the worker can scale down to zero and effectively disappear from the runtime footprint altogether. When new events arrive, Azure Container Apps can automatically scale the worker back out to process the backlog.

This is powered through the built-in KEDA integration that Azure Container Apps provides. Rather than manually building autoscaling infrastructure, I can define scaling rules around queue length and let the platform handle the orchestration automatically.

Features like this reinforce an important theme throughout this architecture journey: scalability is not only about expanding outward into more regions. It is also about scaling inward efficiently when demand is low.

That flexibility matters just as much as geographic redundancy. A platform that can intelligently reduce resource consumption during quiet periods is often more sustainable than one that is simply designed to scale endlessly upward.

Optimizing the Frontend

One optimization I have been considering is moving the React frontend out of Azure Container Apps entirely and hosting it through Azure Blob Storage as a static website. Doing so would eliminate one container app per region and shift static content hosting to a much cheaper service.

Architecturally, this makes sense. The React frontend ultimately compiles into static assets, so continuously running a dedicated container for it may not be the most efficient use of resources. Blob Storage can serve those files reliably and at a significantly lower cost.

However, this introduces operational tradeoffs that are easy to overlook when chasing optimization.

Right now, the frontend deployment is tightly coupled to the rest of the stack. The frontend, backend, and worker are all deployed together through the same Terraform Plan and Apply workflow. That synchronization provides a level of consistency that reduces deployment drift between components.

I also get strong deployment visibility from the containerized approach. The deployed container image version clearly identifies which frontend build is running in a given environment.

Moving to Blob Storage changes that model completely.

Instead of deploying immutable container images, I would be copying static assets into mutable storage. The frontend deployment would move out of Terraform and into a separate GitHub Actions workflow responsible for publishing files into Blob Storage after merges to main.

At that point, the frontend and backend deployments become decoupled systems.

The backend and worker would still deploy during Terraform execution, while the frontend would deploy independently through GitHub Actions. That creates the possibility of the frontend drifting out of sync with backend APIs simply because the deployment mechanisms are now separate.

Managing the Complexity of Decoupled Deployments

This is the kind of complexity that rarely shows up in architecture diagrams.

On paper, moving static assets into Blob Storage looks like a simple optimization. In practice, every optimization introduces new operational considerations. Eliminating infrastructure can sometimes increase coordination complexity elsewhere in the system.

To reduce the risk of deployment drift, I could introduce approval gates into the GitHub Actions workflow similar to the approval process Terraform already provides. That would help synchronize frontend releases with backend infrastructure changes.

There is also the question of deployment traceability.

With container apps, runtime configuration clearly exposes the deployed image version. Blob Storage does not naturally provide that same visibility. To determine which frontend version is currently deployed, I would likely need to rely on GitHub Actions history and deployment metadata to trace which commit hash published the static assets.

None of these problems are unsolvable. They are simply the tradeoffs that emerge when optimizing systems beyond their initial architecture.

Building for Tomorrow Without Overpaying Today

What I find most interesting about this entire exercise is that scaling down did not invalidate the original architectural decisions.

The platform is still designed for multi-region active-active deployment. The infrastructure patterns still support regional expansion. Terraform still allows me to scale outward quickly if usage, traffic, or customer requirements demand it.

The difference is that I am no longer paying for infrastructure scale that I do not currently need.

That is the real lesson I took away from building this platform.

You do not need multi-region deployment on day one. You need the ability to grow into it when the time comes.

There is enormous value in designing systems that can scale globally while operating economically at smaller scale. In many cases, the best architecture is not the one running the most infrastructure. It is the one that can evolve gracefully without requiring a complete redesign later.

Right now, a single-region deployment is the correct balance for where the platform is today. The important part is knowing that when the platform eventually needs additional regions again, scaling back out is no longer a massive engineering effort. It is simply another Terraform change.

Conclusion

One thing the cloud providers are always happy to help you do is spin more meters. Adding regions, adding replicas, adding redundancy, and adding capacity is often only a few clicks or a small Terraform change away. The platforms are designed to make expansion easy.

But easy does not always mean necessary.

What I have been learning through this process is that good cloud architecture is not just about proving that you can scale. It is about understanding when you actually should scale and designing systems that let you make that decision deliberately.

There will always be some unavoidable sunk cost in keeping a platform online. Certain foundational resources need to exist whether you have ten users or ten million users. The goal is not eliminating cost entirely. The goal is minimizing unnecessary cost while preserving the ability to expand quickly when demand arrives.

That is the balance I am trying to optimize for.

I want the platform to be capable of scaling outward into additional regions, scaling workloads dynamically based on demand, and evolving without requiring major architectural rewrites. At the same time, I do not want to continuously pay for infrastructure capacity that is sitting idle simply because the architecture might need it someday.

In many ways, that is the real value of infrastructure-as-code and cloud-native design. Not that they let you permanently run at hyperscale, but that they let you move between scales intentionally.

Sometimes scaling up is the right decision. Sometimes scaling down is the right decision. The important thing is building systems flexible enough to do both.