Architecture Azure

Azure Enterprise Architecture: Active-Active Multi-Region Web App

Most teams that say “we’re multi-region” are describing a warm standby they have never failed over to. This reference architecture is about the harder, more valuable thing: a web application where two or more Azure regions take live production traffic at the same time, serve users from whichever region is closest and healthy, and continue serving when an entire region goes dark — with no DNS change, no pager-driven promotion, and no business decision in the critical path. It is a pattern that scales down to a two-region SaaS startup and up to a global enterprise platform, and the same component set serves both; what changes is the dial settings, not the diagram.

This article follows the format of the major architecture centers: the scenario, the end-to-end flow, a component-by-component breakdown, concrete implementation and IaC wiring, the enterprise concerns (security, cost, reliability, observability, governance), a named worked example with real numbers, and an honest section on when not to build this.

The business scenario

Picture a company running a customer-facing web platform — a B2B portal, an e-commerce storefront, a SaaS product, a booking engine. It started in one Azure region with zone redundancy, which is a genuinely good baseline: it survives a single datacenter failure inside the region. But the business has crossed a threshold where that is no longer enough, and the threshold looks the same whether the company is small or large:

The problem this architecture solves is precise: deliver low-latency reads and writes to a global user base while tolerating the loss of an entire Azure region with near-zero recovery time and a recovery point measured in seconds — and prove it continuously by keeping both regions live. The non-goals matter too. This is not a “five nines at any cost” exercise; it is the minimum coherent design that makes a region a disposable unit, and it is blunt about the latency and money you pay for the write side.

Architecture overview

The organizing idea is the deployment stamp: a complete, independently healthy copy of the application in one region, with a thin, stateless, globally-distributed layer above it and a globally-replicated data layer beneath it. You build the stamp once as a parameterized module and instantiate it in each region. The global layer routes; the stamp serves; the data layer replicates. No request a stamp serves ever makes a synchronous call into another region — that single rule is what keeps one regional outage from becoming two.

The request path, end to end:

  1. A user’s browser resolves the application’s hostname. The DNS answer is an Azure Front Door anycast endpoint, so the client connects to the nearest Microsoft edge POP (point of presence) by BGP, not by geography you configured.
  2. At the edge POP, Front Door terminates TLS, applies the WAF policy (OWASP managed rules, bot protection, rate limiting), serves any cacheable static asset from the edge, and — for dynamic requests — selects an origin. Both regional stamps sit in one origin group at equal priority, so latency-based routing sends the request to the closest origin that is currently passing its health probe.
  3. The request arrives at the chosen region’s stamp ingress — an Application Gateway (with WAF) in front of an AKS cluster, or directly at an App Service environment — over a Private Link / private origin so the stamp is never publicly reachable except through Front Door.
  4. The app tier (App Service or AKS pods) handles the request entirely in-region: it reads and writes the regional Azure Cache for Redis for session and hot data, reads secrets from Key Vault via managed identity over a private endpoint, and reads/writes the data layer.
  5. The data layer is Azure Cosmos DB configured for multi-region writes: the app writes to the local region’s Cosmos endpoint and gets single-digit-millisecond acknowledgement, while Cosmos asynchronously replicates the write to the other region(s) and resolves any conflicts by the policy you chose. For relational state that cannot move to Cosmos, an Azure SQL failover group provides a single-writer geo-replicated alternative (covered in the component breakdown).
  6. The response returns up the same path; Front Door may add it to the edge cache per your caching rules.

The role of Traffic Manager. Front Door is the right global front for HTTP/S because it decides per request at the edge and drains an unhealthy origin in seconds with no DNS TTL to wait out. Azure Traffic Manager is a DNS-based global load balancer and belongs in this architecture for the cases Front Door cannot cover: non-HTTP protocols (e.g. a regional API over raw TCP, a messaging endpoint), or as the outermost layer in a “Front-Door-of-Front-Doors” pattern where you want DNS-level steering across more than one Front Door profile. Its failover is bound by DNS TTL (tens of seconds to minutes depending on client resolvers), so you keep it off the latency-critical HTTP path and use it deliberately where DNS steering is the only option.

If you sketch this on a whiteboard it is three horizontal bands: a global band (Front Door + WAF, optionally Traffic Manager above it), a pair (or trio) of identical regional stamp boxes side by side, and a data band spanning all regions (Cosmos DB multi-write, drawn as one logical store replicated across the stamps). Arrows flow top to bottom for requests and horizontally across the data band for replication — and crucially, there are no horizontal arrows in the stamp band.

Azure active-active multi-region web app: global Front Door + WAF edge (with optional Traffic Manager) routing to two identical regional deployment stamps (App Gateway + WAF, App Service/AKS, Cache for Redis, Key Vault), over a Cosmos DB multi-write data layer with async cross-region replication and an Azure SQL failover-group alternative.

Component breakdown

Each component earns its place by doing exactly one job in the topology. The table summarizes; the prose below it covers the decisions that bite.

Component Role in the architecture Key configuration choices
Azure Front Door (Premium) Global anycast ingress, TLS, WAF, edge caching, per-request origin selection Both stamps in one origin group at equal priority + equal weight; latency routing; deep health probe; Premium SKU for Private Link origins and managed WAF
Azure Traffic Manager DNS-level global steering for non-HTTP or multi-profile cases Performance or Priority routing; low TTL; kept off the HTTP critical path
Web Application Firewall L7 protection at the edge (and optionally at the stamp) OWASP managed rule set in Prevention mode, bot manager, per-IP rate limits, custom rules for known-bad patterns
Application Gateway + WAF Regional ingress / L7 load balancer in front of AKS WAF_v2, autoscaling, zone-redundant; only needed for AKS stamps (App Service needs no fronting LB)
Azure App Service PaaS app-tier stamp option Premium v3, zone-redundant, autoscale rules, regional VNet integration, deployment slots for safe rollout
Azure Kubernetes Service (AKS) Container app-tier stamp option Multi-AZ node pools, cluster autoscaler + KEDA, workload identity, one immutable image digest to every stamp
Azure Cosmos DB Globally-distributed multi-write data store Multiple write regions enabled, Session consistency, deliberate conflict-resolution policy, partition key chosen to localize an entity’s writes
Azure SQL (failover group) Relational alternative: single-writer geo-replica Automatic failover policy, read-scale to secondary via ApplicationIntent=ReadOnly, async replication (RPO in seconds)
Azure Cache for Redis Per-region session / hot-data cache One independent instance per stamp (not geo-linked); zone-redundant; externalizes session so any stamp can serve any user
Azure Key Vault Per-region secrets, keys, certificates Private endpoint, RBAC mode, accessed via managed identity — no secrets in config or pipelines
Azure App Configuration Centralized feature flags and config One source of truth read by both stamps, with regional overrides as labels, to prevent config drift
Azure Monitor / Log Analytics / App Insights Cross-region observability Distributed tracing with region dimension, availability tests from multiple geos, unified alerting

Front Door is the linchpin, and the probe path is its most important setting. The health probe must hit a /health/deep endpoint that exercises the in-region dependencies a request actually needs — the database connection, the cache, a critical downstream — and returns a non-200 the instant any of them is broken. A shallow probe that returns 200 while Cosmos is unreachable will keep routing users to a dead stamp and silently destroy your recovery time. The probe interval and the “samples required to flip state” together set your worst-case detection time; tune them against a real drill, knowing every edge POP probes independently so a tight interval multiplies probe load. Also: disable session affinity for active-active unless you genuinely need sticky sessions — affinity pins a client to one origin and undercuts the entire point of two live regions.

App Service vs AKS is the stamp decision, and it is a real fork. Choose App Service when the team wants the smallest operational surface: it is a PaaS web host, gives you zone redundancy with a checkbox, has deployment slots for safe rollout, and needs no fronting load balancer. Choose AKS when you already run containers, need fine-grained scaling (KEDA on queue depth), run polyglot or non-HTTP workloads in the same cluster, or want portability. The topology above is identical either way — the only difference inside the stamp band is whether the box contains “App Service” or “App Gateway + AKS.” Many enterprises run App Service for the web tier and AKS for back-end services in the same stamp.

Cosmos DB multi-region writes is what makes the write side active-active, and it is where the cost and the subtlety concentrate. Enabling multiple write regions lets the app in each region write locally with millisecond latency, but it means two users can update the same item within the replication window — the system will produce conflicts, and your only choice is to decide their resolution policy or discover it in production. The default Last-Writer-Wins (highest value on a chosen path) converges every region on the same winner, which is correct when writes are idempotent or last-update genuinely wins; but it silently drops the loser and has a sharp edge (in a delete-vs-update conflict, delete wins), which is wrong for money. For balances, inventory, or anything where the losing write must be preserved, register a custom merge stored procedure and monitor the conflicts feed like a dead-letter queue. Two design moves blunt the problem before resolution: partition so an entity’s writes stay regional (route a tenant predominantly to one region — active-active across the fleet, single-writer per entity in practice), and prefer commutative operations (an append-only event stream has nothing to conflict on). Note also that Strong consistency is not available across multiple write regions; Session is the practical default.

The regional Redis caches are deliberately independent, not geo-replicated. Each stamp has its own cache holding session and hot data so that a request served by either region finds what it needs locally; the source of truth lives in Cosmos. Geo-linking the caches would reintroduce a cross-region dependency on the hot path — exactly what the stamp rule forbids.

Implementation guidance

The whole architecture should be expressible as one region-parameterized infrastructure module instantiated per region, plus a thin global module for the Front Door / Traffic Manager layer. Build once, deploy the same immutable artifact to every stamp in a single pipeline run, and let only the inputs differ. That is what keeps the stamps byte-for-byte identical, which is the precondition for either of them serving any request.

IaC structure (Terraform shown; Bicep maps one-to-one with modules and a parameter file per region):

# main.tf — fan the same stamp module across regions, then a global layer above.
locals {
  regions = {
    westeurope    = { sku = "P1v3", cosmos_failover_priority = 0 }
    southeastasia = { sku = "P1v3", cosmos_failover_priority = 1 }
  }
}

module "stamp" {
  source   = "./modules/regional-stamp"
  for_each = local.regions

  location              = each.key
  resource_group_name   = "rg-stamp-${each.key}"
  app_sku               = each.value.sku
  image_digest          = var.image_digest          # SAME digest to every stamp
  app_config_endpoint   = var.app_config_endpoint    # one config source of truth
  cosmos_account_id     = azurerm_cosmosdb_account.app.id
  cosmos_failover_prio  = each.value.cosmos_failover_priority
}

# Cosmos DB spans regions as a single account with multiple write regions.
resource "azurerm_cosmosdb_account" "app" {
  name                = "cosmos-aa-prod"
  resource_group_name = azurerm_resource_group.global.name
  location            = "westeurope"
  offer_type          = "Standard"
  kind                = "GlobalDocumentDB"

  enable_multiple_write_locations = true
  consistency_policy { consistency_level = "Session" }

  geo_location { location = "westeurope"    failover_priority = 0  zone_redundant = true }
  geo_location { location = "southeastasia" failover_priority = 1  zone_redundant = true }
}

# Global ingress: both stamps as equal-priority origins under one origin group.
module "front_door" {
  source        = "./modules/global-frontdoor"
  origins       = { for k, m in module.stamp : k => m.private_origin_host }
  probe_path    = "/health/deep"
  probe_interval = 30
  waf_mode      = "Prevention"
}

Three implementation rules carry most of the weight:

Networking and identity wiring. Each stamp gets its own VNet with private endpoints for Cosmos, Key Vault, Redis, and SQL, so app-to-data traffic never traverses the public internet. Front Door reaches the stamp over a Private Link origin (Front Door Premium feature), meaning the stamp’s ingress (App Gateway or App Service) is not publicly reachable — the only public surface in the entire system is the Front Door anycast endpoint. Identity is managed-identity-first: the app tier authenticates to Cosmos, Key Vault, and SQL with a system- or user-assigned managed identity and Azure RBAC / Entra-based data-plane roles, so there are no connection-string secrets in pipelines or config. Cosmos itself supports disabling key-based auth entirely and using Entra RBAC for the data plane — do that.

Enterprise considerations

Security and Zero Trust. The architecture is Zero-Trust-shaped by construction: a single public entry point (Front Door), WAF at the edge in Prevention mode, private endpoints for every data service, and managed identity rather than secrets for every service-to-service call. Apply the assume-breach posture at the data tier with Cosmos Entra RBAC (and key auth disabled), Key Vault in RBAC mode behind a private endpoint, and Microsoft Defender for Cloud plan coverage on App Service/AKS, Cosmos, Key Vault, and SQL. For AKS stamps, use workload identity (not pod-managed-identity v1), private clusters, and Azure Policy for Kubernetes to enforce baselines. Network segmentation is per-stamp, so a compromise in one region’s VNet does not reach the other.

Cost optimization. Active-active is the expensive end of resilience, and honesty about cost is part of the design. You are paying for: roughly 2x compute (both regions run real capacity, not a cold standby), Cosmos multi-write RU/s in every write region plus cross-region replication egress, Front Door Premium, and per-region Redis/Key Vault. Levers that materially reduce the bill without breaking the pattern: run each stamp at the capacity it needs for its share of traffic plus failover headroom (not full global capacity in each — see the worked example for the math), use autoscale so the failover headroom is provisioned only when a region actually absorbs the other’s load, put Cosmos on autoscale RU/s so you pay for provisioned throughput by usage, lean on Front Door edge caching to keep dynamic origin calls (and therefore RU consumption) down, and apply reservations / savings plans to the steady-state compute and Cosmos floor. The decision to enable multi-write (versus a cheaper geo-replicated single-writer) is the single biggest cost fork — make it only where both regions genuinely need to accept writes.

Scalability. Each stamp scales independently and horizontally: App Service autoscale rules or AKS cluster-autoscaler + KEDA on the compute tier, Cosmos partition-based throughput on the data tier (choose a partition key with high cardinality and even access so no single partition becomes a hot shard), and Front Door + edge caching absorbing read-heavy spikes before they reach a region. Because the stamps are identical and parameterized, adding a third region is a config change — add an entry to the regions map and a geo_location to Cosmos — which is the scalability story that matters at enterprise size: growth is a parameter, not a project.

Reliability and DR (RTO/RPO). This is the payoff, and the two numbers come from two different tiers — never conflate them. RTO (how long until service resumes) is governed by Front Door’s probe cadence: the edge drains a sick origin in two-to-three probe cycles with no DNS TTL involved, so RTO is seconds for the HTTP data plane. RPO (how much data you can lose) is governed entirely by the data tier’s replication model, not by routing: Cosmos multi-write gives near-zero RPO with conflict handling, while an Azure SQL failover group replicates asynchronously and admits an RPO of seconds on a hard regional loss. The table makes the trade explicit:

Data model Write topology RPO on regional loss When to use
Zone-redundant only Single region N/A (dies with region) Baseline HA floor; not multi-region
Azure SQL failover group Active-passive (one writer) Seconds (async lag) Relational state; cheaper; tiny data-loss window acceptable
Cosmos DB multi-write Active-active (all writers) Near zero (+ conflict policy) True active-active writes; lowest RPO; highest cost/complexity

Because both regions are always live, the failover is exercised by every request — there is no stale standby to rot. The remaining orchestration is the stateful failover (promoting a SQL primary) and failback, both of which belong in a human-gated runbook, never an automatic reflex on a green probe. And the only way to trust any of this is to rehearse it on a schedule — disable one origin at the edge and confirm traffic continues from the survivor, then escalate to a forced data-tier failover in a drill, using Azure Chaos Studio for repeatable fault injection (NSG block, AKS pod kill, VM shutdown). A drill that reveals real RTO is 90 s against a 30 s target is a success: you found the gap in a controlled window.

Observability. Instrument every request with distributed tracing (App Insights / OpenTelemetry) carrying a region dimension, so a latency or error spike can be attributed to a specific stamp. Run availability tests from multiple geographies through the Front Door endpoint every minute (synthetic canaries) — this is what catches a stamp that is up but unhealthy before users do. Alert on the signals unique to this topology: origin health-flip at Front Door, Cosmos replication latency and conflict-feed depth, SQL failover-group replication lag, and any non-zero terraform plan (config drift). Aggregate logs to a single Log Analytics workspace (or a workspace per region federated for query) so on-call can reason across both regions in one place.

Governance. Wrap the whole thing in Azure Policy initiatives that enforce the invariants the architecture depends on: resources must be zone-redundant, data services must have private endpoints (no public network access), Cosmos key auth must be disabled, regions must be from an approved paired/near-region list, and tags for cost center and environment are mandatory. Manage the two regions’ resources under a management-group / landing-zone structure so policy, RBAC, and budget alerts apply uniformly, and so adding a region inherits the guardrails automatically. Budget alerts at the subscription and per-stamp resource-group level keep the active-active cost visible to the team that owns it.

Reference enterprise example

Northwind Bookings is a fictional mid-market travel-reservations SaaS: a web platform where travel agencies search and book inventory. Pre-project they ran a single zone-redundant stamp in West Europe. The forcing event was twofold — a four-hour West Europe degradation that breached three enterprise customers’ 99.95% SLAs (a six-figure credit), and a strategic push into the APAC market where agencies in Singapore and Sydney were abandoning searches over the 250 ms latency. Their target, signed off by finance against the SLA-credit math: RTO under 60 seconds, RPO under 5 seconds, p95 search latency under 80 ms in both Europe and APAC.

What they built. Two stamps — West Europe and Southeast Asia — each an App Service Premium v3 web tier (zone-redundant) plus an AKS cluster for the pricing/availability back-end services, fronted by Front Door Premium with both stamps as equal-priority Private Link origins and a /health/deep probe that checks Cosmos, Redis, and the inventory downstream. Booking and availability state moved to Cosmos DB with multi-region writes, Session consistency, partitioned by agencyId so a given agency’s writes stay predominantly in its home region. Their legacy financial-ledger stayed on Azure SQL in a failover group (single-writer, async) because the accounting team required strict relational integrity and accepted a seconds-level RPO for that subsystem. Redis per stamp held session and search-result cache; Front Door edge-cached the static catalog and search assets.

A decision that bit, and the fix. Their first Cosmos conflict policy was the default Last-Writer-Wins. In a load test they found that a cancellation (modeled as a delete) racing a concurrent seat-change (an update) in the other region let the delete win under LWW — a booking vanished that should have been amended. They moved the booking collection to a custom merge stored procedure that never lets a stale delete beat a newer update, alerted on conflict-feed depth, and — for the seat-inventory counter specifically — switched to a commutative append model so there was nothing to overwrite. This is the canonical active-active lesson: the default conflict policy is a business decision, not a database setting.

The numbers.

Dimension Before (single region) After (active-active)
Regions serving live traffic 1 (West Europe) 2 (West Europe + Southeast Asia)
p95 search latency, APAC ~250 ms ~70 ms
Measured RTO (edge drain, drilled) hours (manual DR) ~35 s
Measured RPO (Cosmos / SQL) N/A ~0 s Cosmos / ~3 s SQL ledger
Compute capacity provisioned 1.0x ~1.4x (each stamp at 70% of global peak + autoscale headroom)
Monthly platform cost baseline ~1.8x baseline

The capacity decision is the one worth copying: rather than running 2.0x (full global capacity in each region), Northwind provisioned each stamp at 70% of global peak with autoscale configured to absorb the other region’s load on failover. In steady state both regions share traffic and run comfortably; if one fails, the survivor autoscales from 70% toward 100%+ within minutes — accepting a brief degradation window in exchange for not paying for idle 2x capacity year-round. The outcome after six months: zero SLA breaches across two real regional blips (both drained automatically with no customer-visible impact), APAC search-abandonment down sharply, and a quarterly Chaos Studio game day that keeps the failover path honest.

When to use it

Use this architecture when uptime is contractually or financially load-bearing (an outage is an SLA breach, not an apology), your users are genuinely multi-region and latency is a revenue lever, a region-level failure is in your threat model, and — critically — the business will fund roughly 1.8–2x the single-region cost and the team has the operational maturity to run two live regions and rehearse failover. It scales cleanly from a two-region startup to a multi-region global enterprise; the diagram is the same, only the dials change.

Trade-offs to accept going in. You are paying for near-2x compute, multi-write Cosmos RU/s plus replication egress, and the cognitive load of conflict handling and distributed-system reasoning. The write side in particular is where complexity concentrates: if both regions accepting writes is not a hard requirement, you can often hit the target far more cheaply.

Anti-patterns that quietly defeat the design:

Alternatives, in increasing capability and cost: (1) Single region, zone-redundant — the right baseline; survives a datacenter, not a region. (2) Active-passive multi-region (warm standby, Azure SQL failover group, Front Door priority routing) — automated failover in tens of seconds, far cheaper, the pragmatic default for most apps that need regional resilience but not active-active writes. (3) Active-active reads, single-writer — both regions serve reads from a geo-replica while writes funnel to one region; most of the latency win, none of the conflict complexity. (4) This architecture — active-active multi-write — the full pattern, for when both regions must accept writes with near-zero RPO. Pick the lowest tier that meets the number the business will actually fund, and prove it on a schedule. The architecture you have rehearsed beats the architecture you have merely drawn.

AzureArchitectureEnterpriseReference Architecture
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading