Search Tech Journey

Find topics, journeys and posts

6-month learning plan76 / 130
back to blog
systemsintermediate 55m read

S076 · Multi-Region — Active-Passive, Active-Active, Failover

Surviving whole-region outages without lying to your users. The three architectures, the two RTO/RPO knobs, and why 'active-active' is often a marketing term.

⚙️SystemsM08 · Distributed Systems· Session 076 of 130 90 min

🎯 Choose an active-passive / pilot-light / active-active topology given RTO/RPO targets, and design a failover plan you'd actually trust at 3 AM.

Why this session exists

Once a year AWS us-east-1 has a bad day, Azure has a Front Door incident, or GCP loses networking in a region. Companies with real multi-region designs shrug and route around it in minutes. Companies without pay for it in headlines, refunds, and CEO apologies. Multi-region is expensive, complicated, and — done wrong — actually increases your outage surface. This session gives you the mental model to decide whether you need it, and the vocabulary to design it if you do.

You will be able to
  • Define RTO and RPO in plain English and set numeric targets for a hypothetical product.
  • Compare active-passive, pilot-light, warm-standby, and active-active on cost / RTO / complexity.
  • Explain why data replication is the hard part and why 'sync replication across continents' is usually a bad idea.
  • Design a DNS-based failover plan including health checks, TTLs, and the split-brain risk.
  • List the three biggest failure modes of multi-region (data divergence, DNS lag, dependency asymmetry).

Prerequisites

  • S068 · Replication basics — the primitives you'll wire together across regions.
  • S072 · Consistency models — cross-region consistency is where the physics of light bites.
  • S073 · Consensus — Raft/Paxos across regions is 100+ ms per write; know why.


(a) Intuition · 5 min

Backup power for a hospital
🌍 Real world

A hospital has three options against a citywide power outage: batteries in a closet (turn on manually, 15 min gap in lights), an idle generator (auto-starts, 30 s gap), or two independent grids running in parallel (zero gap, twice the electricity bill).

Every option is legitimate. Which you pick depends on what happens in the gap. Surgeries in progress? Zero-gap only. Office lighting? Batteries are fine.

💻 Code world

Multi-region is exactly this menu. Backup-and-restore: your data is in another region as backups, you rebuild the app if the primary dies (hours). Pilot-light: a minimal always-on copy in another region; you scale it up on failover (10-30 min). Warm standby: a smaller-but-live copy ready to take traffic (1-5 min). Active-active: both regions serve production traffic all the time (seconds, if done well).

The right choice depends on two numbers: RTO (how long can you be down?) and RPO (how much data can you lose?). Every architecture is a specific point on the cost/RTO/RPO surface.

RTO and RPO — the two numbers everything else derives from

The whole conversation is about these two
  • RTO (Recovery Time Objective) — how long the outage can last before it materially hurts the business. 'The site can be down for 4 hours' means RTO = 4h.
  • RPO (Recovery Point Objective) — how much data you can lose. 'We can afford to lose the last 15 minutes of writes' means RPO = 15 min.
  • RTO=0 requires active-active (or damn close). RPO=0 requires synchronous cross-region replication (expensive, high latency).
  • Most SaaS lands at RTO=30 min, RPO=5 min — achievable with warm standby + async replication.
  • Financial ledgers push RPO to zero — synchronous multi-region commits — and eat 100+ ms per write.

A quick history — how we got here

  1. 2005
    AWS us-east-1 launches
    For a decade, 'multi-region' meant 'use another cloud'. Nobody actually did it.
  2. 2012
    Netflix Chaos Monkey → Simian Army
    Netflix decides to intentionally test region failures. Multi-region becomes the differentiator between 'available' and 'sometimes available'.
  3. 2015
    AWS Route 53 health checks + failover
    DNS-based failover becomes cheap and pushbutton. Small companies start doing it too.
  4. 2017
    AWS S3 us-east-1 outage
    S3 down for 4 hours; half the internet breaks. Wake-up call: even 'the AWS control plane' has regional dependencies.
  5. 2019
    Google Cloud Spanner GA
    First mainstream globally-consistent SQL database. Trades latency for global RPO=0.
  6. 2021
    AWS us-east-1 major outage
    Multi-hour incident. Companies with multi-region were fine; companies without lost the entire day.
  7. 2024
    Cross-region as default in serverless
    DynamoDB Global Tables, Aurora Global Database, S3 Multi-Region Access Points — 'multi-region' is now a checkbox.

(b) Visual walkthrough · 15 min

The four topologies

The failover sequence — what actually happens at 3 AM

The five moving parts of a multi-region design

1routing
1 · Global traffic entry

DNS-based (Route 53, Cloudflare) or Anycast (CloudFront, Cloud Load Balancer). Determines how fast failover propagates.

2compute
2 · Application layer

Stateless services in each region. Easy — just deploy the same containers.

3data
3 · Data layer

The hard part. Async replication (RPO > 0) or synchronous consensus (RPO=0, high latency).

4session
4 · Session / auth state

Where does a user's session live? Sticky-to-region or replicated? Often forgotten until failover.

5process
5 · Runbook + gameday

The plan to declare failover, the tested procedure to flip, and the practice runs. Untested plans fail.

Data layer choices — the entire complexity

How data crosses regions

Async logical replication (default)
Postgres logical / MySQL binlog / DynamoDB Streams shipped to the other region. RPO = replication lag (usually seconds to minutes).
async
Synchronous consensus (Spanner, CockroachDB)
Every write commits across regions via Raft/Paxos. RPO=0, but write latency = round-trip * 2. Only acceptable for truly critical data.
sync
Multi-master with conflict resolution
Both regions accept writes. Conflicts resolved by last-writer-wins, CRDTs, or app-level merge. Cassandra, DynamoDB Global Tables, Riak.
multi-master
Partitioned by region (Netflix model)
Each user is 'owned' by a home region. Writes always go home. Cross-region reads are async. No conflicts.
partitioned
Backups only
Nightly snapshot to another region's storage. RPO = 1 day. Cheap and dumb — sometimes exactly right.
backup

Side by side — three real production topologies

Netflix (active-active × 3)

Each region can serve any user; users pinned to nearest home

  • 3 AWS regions active
  • Cassandra replicated with tunable consistency
  • Chaos Monkey / Simian Army test regional failure weekly
  • Cost: 3x infrastructure for 1x traffic (over-provisioned)
  • Reward: entire region can die with ~2 min customer impact
Typical SaaS (warm standby)

Primary + secondary, async replication

  • 1 primary region running production
  • 1 secondary region with app deployed but scaled down
  • Async DB replication (RPO ~30s to 5min)
  • Failover triggered manually via runbook (RTO 10-30 min)
  • Cost: ~1.2x infra + engineering time
Banking / financial (synchronous)

Ledger MUST be RPO=0

  • Ledger DB uses Spanner / CockroachDB / synchronous mirroring
  • Every write acks after cross-region quorum (~50-100 ms)
  • App is warm-standby (RTO minutes)
  • Read-only replicas per region for query performance
  • Cost: 3-5x, unavoidable — money can't be RPO > 0

The mental model to hold


Common misconception
✗ What most people think

"Deploying to a second region gives me high availability. If one region fails, traffic moves to the other and users barely notice."

✓ What is actually true

A second region gives you the capability to fail over. Whether you actually can depends on things most teams have not verified: does the second region have enough capacity to take 100% of traffic, is its data current, does failover require a control-plane operation that may itself be affected, and has anyone executed it recently? An untested failover path is a hypothesis, and a multi-region deployment where the second region has never served production traffic is a very expensive hypothesis.

Why the myth is so sticky

The myth is sticky because the deployment topology is visible and checkable — resources exist in two regions, the diagram has two boxes — while the failover procedure is invisible until executed. It is also true that the hard infrastructure work is genuinely done. What remains undone is everything that only reveals itself under a real failover: a hardcoded regional endpoint, a certificate provisioned in one region, a quota never raised in the standby, a database whose promotion takes far longer than anyone assumed.

Prove it to yourself

The only test that means anything is executing the failover:

Questions a game day answers and a diagram cannot:

1. Does region B have quota for 100% of traffic, right now?
2. How long does the database promotion actually take?
3. Does failover need the control plane, which may be degraded?
4. What is the replication lag at this moment (= data loss)?
5. Can you fail BACK, or is this one-way?

If you cannot answer all five from a recent test,
you have a standby region, not a failover plan.
From first principles
Start with the question

Why is active-active multi-region so much harder than active-passive, when both have the same infrastructure in both places?

  1. 1
    Active-passive has exactly one region accepting writes at any time, so there is a single authority on write order.
    forced by · only one node accepts writes, which makes ordering a local property requiring no coordination
  2. 2
    Active-active accepts writes in both regions concurrently, so two writes to the same entity may be accepted independently with no knowledge of each other.
    forced by · coordinating on every write would cost a cross-region round trip, which is the exact latency active-active exists to avoid
  3. 3
    Therefore conflicts are possible, and there is no globally correct resolution available after the fact — the system must be told what to do.
    forced by · both writes were accepted and acknowledged, so neither can be declared invalid without violating something already promised to a client
  4. 4
    Conflict resolution is application semantics, not infrastructure: last-writer-wins silently discards a real change, merging requires knowing what the fields mean, and some operations cannot be merged at all.
    forced by · only the application knows whether two concurrent updates to a record are compatible or mutually exclusive
  5. 5
    So active-active pushes a genuinely hard, domain-specific problem into every write path, whereas active-passive eliminates it entirely by construction.
    forced by · a problem that cannot occur needs no solution, and single-writer makes concurrent conflicting writes impossible
⇒ Therefore

Therefore the difficulty of active-active is not operational complexity — it is that it converts a solved problem (write ordering) into an unsolved one (conflict resolution) that must be answered per data type.

And note what this predicts: active-active is tractable exactly where writes can be partitioned by region so conflicts cannot arise — each user's data has a home region, and writes for that user always go there. That is why real active-active deployments are usually partitioned active-active rather than truly symmetric. It also predicts that append-only and commutative data types are far easier to run active-active than mutable records, which is why event-shaped designs appear so often in multi-region systems.

Mental modelFailover is a procedure, not a property

Multi-region availability is not something your architecture has; it is something a sequence of steps achieves, executed under stress, possibly by someone who has not done it before. Every step that requires a human decision, a control-plane call, or a DNS change is a step that can fail or be slow at exactly the wrong time.

Measure it the way the business does: RTO is how long recovery takes, RPO is how much data is lost. Both are numbers you must be able to state and have recently demonstrated.

  • Failover must not depend on the failing region, and must not depend on a control plane that may be affected. Anything requiring resource creation during an incident is unavailable exactly when needed — pre-provision standby capacity rather than planning to scale it up.
  • Traffic steering mechanisms have very different characteristics: DNS is universally supported and slow to take effect because of caching you do not control; anycast and global load balancers shift in seconds but cost more and are themselves a dependency. Know which you have and what its actual switch time is.
  • Replication lag is your RPO expressed as a live metric. It is not a target in a document — it is the current amount of data you would lose if the region failed this second, and it should be graphed and alerted on for that reason.
  • Test failover on a schedule, in production, with the real procedure. A failover path exercised only during incidents is a path with unknown correctness, and every untested assumption in it will be discovered at the worst possible moment.
🔔 Fires when you see

Fire this model when you see: a standby region that has never taken traffic · a runbook last updated a year ago · a DR plan requiring resource creation · a regional endpoint hardcoded in configuration · nobody able to state the current RPO.

The tradeoff

Active-passive with failover, or active-active with traffic in both regions?

Active-passive
+ you gain one region accepts writes, so there are no conflicts and the data model needs no changes at all. Reasoning about correctness is identical to a single-region system, which is an enormous simplification.
− you pay the standby is paid for and idle, failover takes real time during which you are down, and the path is only as reliable as its last test. Capacity in the standby tends to drift below what is needed because it is never exercised.
pick when most systems: when an RTO of minutes is acceptable and the write model does not tolerate conflicts
Active-active, partitioned by region
+ you gain both regions serve traffic so nothing is idle, users get low latency from their nearest region, and losing a region degrades capacity rather than causing an outage. Failover for the affected partition is the only recovery needed.
− you pay you must partition data by home region and route each user consistently, which constrains the data model and complicates any cross-region operation. Users whose home region fails still experience a failover.
pick when when users are geographically clustered and their data is naturally separable — which is common in consumer products and rare in enterprise ones
Active-active, fully symmetric
+ you gain maximum availability and lowest latency, with no failover step at all because both regions already serve everything. Losing a region is a capacity event rather than an incident.
− you pay full conflict resolution must be designed into every mutable entity, and the reasoning burden is permanent — every new feature must consider concurrent cross-region writes. This is among the hardest things to build and maintain correctly.
pick when only when availability requirements genuinely justify it and the data model is naturally commutative or append-only
What a senior engineer actually does

Active-passive with a genuinely tested failover beats active-active that nobody fully understands. The second region's value comes almost entirely from whether the failover works, and a simple design that has been exercised is worth far more than a sophisticated one that has not.

Move to partitioned active-active when latency for distant users becomes a real product problem, and treat fully symmetric active-active as a specialist choice requiring a data model built for it from the start. Whichever you choose, the discipline that matters most is the same: schedule a failover exercise, run it in production, and treat any step that surprises you as a defect to fix rather than a quirk to remember. The regions are the easy part; the procedure is the product.


(c) Hands-on · 25 min

Let's simulate a two-region deployment with async replication, then trigger a failover and measure the RPO (data lost) and RTO (time to recovery).

#!/usr/bin/env python3
"""multi_region_sim.py — a toy simulation of an active-passive setup
with async replication, health checks, and failover.
 
Run:  python multi_region_sim.py
No dependencies.
 
Simulates:
  - Region A (primary): accepts writes, replicates to B
  - Region B (secondary): reads only, tracks replication lag
  - A user client that fails over to B based on health checks
  - We kill A partway through and measure lost writes (RPO) and
    time-to-first-successful-write on B (RTO).
"""
from __future__ import annotations
 
import random
import threading
import time
from dataclasses import dataclass, field
 
 
@dataclass
class Region:
    name: str
    is_primary: bool = False
    healthy: bool = True
    accepts_writes: bool = False
    store: dict[int, str] = field(default_factory=dict)
    # Async replication queue: (msg_id, value, ts_written)
    replication_queue: list[tuple[int, str, float]] = field(default_factory=list)
    lock: threading.Lock = field(default_factory=threading.Lock)
 
    def write(self, key: int, value: str) -> bool:
        if not (self.accepts_writes and self.healthy):
            return False
        with self.lock:
            self.store[key] = value
            self.replication_queue.append((key, value, time.time()))
        return True
 
    def read(self, key: int) -> str | None:
        if not self.healthy:
            return None
        with self.lock:
            return self.store.get(key)
 
    def apply_replicated(self, key: int, value: str) -> None:
        with self.lock:
            self.store[key] = value
 
 
class Replicator:
    """Streams from a source region's queue to a target region's apply()."""
    def __init__(self, source: Region, target: Region, lag_ms: int = 100):
        self.source, self.target = source, target
        self.lag_ms = lag_ms
        self.stopped = False
        self.applied_count = 0
        threading.Thread(target=self._loop, daemon=True).start()
 
    def _loop(self) -> None:
        while not self.stopped:
            time.sleep(0.02)
            with self.source.lock:
                if not self.source.healthy:
                    # Source is down — replication halts. This is the whole point.
                    continue
                batch = list(self.source.replication_queue)
                self.source.replication_queue.clear()
            for key, value, ts in batch:
                # Simulate network latency between regions.
                time.sleep(self.lag_ms / 1000.0 / max(len(batch), 1))
                self.target.apply_replicated(key, value)
                self.applied_count += 1
 
    def stop(self) -> None:
        self.stopped = True
 
 
class HealthChecker:
    """Checks a region every check_interval; declares unhealthy after
    fail_threshold consecutive failures."""
    def __init__(self, region: Region, check_interval: float = 0.5,
                 fail_threshold: int = 3):
        self.region = region
        self.interval = check_interval
        self.threshold = fail_threshold
        self.consecutive_failures = 0
        self.declared_unhealthy = False
        self.stopped = False
        threading.Thread(target=self._loop, daemon=True).start()
 
    def _loop(self) -> None:
        while not self.stopped:
            time.sleep(self.interval)
            if self.region.healthy:
                self.consecutive_failures = 0
                self.declared_unhealthy = False
            else:
                self.consecutive_failures += 1
                if self.consecutive_failures >= self.threshold:
                    self.declared_unhealthy = True
 
    def stop(self) -> None:
        self.stopped = True
 
 
class Client:
    """Writes to whichever region the health checker declares healthy."""
    def __init__(self, primary: Region, secondary: Region,
                 hc_primary: HealthChecker, hc_secondary: HealthChecker):
        self.primary, self.secondary = primary, secondary
        self.hc_p, self.hc_s = hc_primary, hc_secondary
        self.writes_ok = 0
        self.writes_failed = 0
 
    def write(self, key: int, value: str) -> None:
        if not self.hc_p.declared_unhealthy and self.primary.write(key, value):
            self.writes_ok += 1
            return
        # Failover — try secondary
        if not self.hc_s.declared_unhealthy and self.secondary.write(key, value):
            self.writes_ok += 1
            return
        self.writes_failed += 1
 
 
def main() -> None:
    region_a = Region("region-a", is_primary=True, accepts_writes=True)
    region_b = Region("region-b", is_primary=False, accepts_writes=False)
 
    repl = Replicator(region_a, region_b, lag_ms=200)
    hc_a = HealthChecker(region_a, check_interval=0.3, fail_threshold=3)
    hc_b = HealthChecker(region_b, check_interval=0.3, fail_threshold=3)
    client = Client(region_a, region_b, hc_a, hc_b)
 
    print("Phase 1: normal operations for 2s")
    start = time.time()
    key = 0
    while time.time() - start < 2.0:
        client.write(key, f"v{key}")
        key += 1
        time.sleep(0.005)
    print(f"  writes_ok={client.writes_ok}, "
          f"region_a store size={len(region_a.store)}, "
          f"replicated to B={repl.applied_count}")
 
    print("\nPhase 2: KILL region A")
    region_a.healthy = False
    kill_time = time.time()
    last_replicated_at_kill = repl.applied_count
    a_size_at_kill = len(region_a.store)
 
    # Wait for health check to declare A unhealthy, then failover B
    while not hc_a.declared_unhealthy:
        time.sleep(0.05)
    detect_time = time.time()
    print(f"  A declared unhealthy after {(detect_time - kill_time)*1000:.0f}ms")
 
    # Enable writes on B (manual failover step)
    region_b.accepts_writes = True
    failover_time = time.time()
    print(f"  B accepting writes at t={(failover_time - kill_time)*1000:.0f}ms after A died")
 
    # RPO measurement: how many writes were on A but not yet replicated to B?
    rpo_lost_writes = a_size_at_kill - repl.applied_count
    print(f"  RPO: {rpo_lost_writes} writes were on A but never made it to B")
 
    print("\nPhase 3: continue writing (now to B)")
    first_success_after_failover = None
    for _ in range(200):
        before = client.writes_ok
        client.write(key, f"v{key}")
        key += 1
        if first_success_after_failover is None and client.writes_ok > before:
            first_success_after_failover = time.time()
        time.sleep(0.005)
 
    if first_success_after_failover:
        rto = first_success_after_failover - kill_time
        print(f"  RTO: {rto*1000:.0f}ms from A-dies to first successful write on B")
 
    print(f"\nFinal: writes_ok={client.writes_ok}, writes_failed={client.writes_failed}")
 
    repl.stop(); hc_a.stop(); hc_b.stop()
 
 
if __name__ == "__main__":
    main()

What each block does

Anatomy of the script

Region dataclass
Store + replication queue + healthy flag + accepts_writes flag. The last is the failover 'promote' button.
state
Replicator._loop
Async replication with configurable lag (200 ms here). When source dies, replication halts — reproducing the real 'in-flight writes never delivered' bug.
replication
HealthChecker · fail_threshold
Real DNS-based failover has similar logic: N consecutive failures before declaring unhealthy. This is what avoids flapping on a single blip.
detection
Client.write · failover logic
Try primary; on failure OR if HC declared unhealthy, try secondary. Real clients often need retries + timeouts here too.
routing
RPO measurement
Count of writes on A that never replicated to B. This is the real 'data lost' number. In a real system this is customer-visible: 'my order disappeared'.
RPO
RTO measurement
Wall time from 'A dies' to 'first successful write on B'. Includes detection lag + failover promotion + client discovery.
RTO
Try itAdd a split-brain and observe the divergence

Rewrite Client.write to pick randomly:

region = random.choice([self.primary, self.secondary])
if region.write(key, value):
    self.writes_ok += 1

Run and diff the two stores at the end:

diff = {k for k in region_a.store if region_a.store.get(k) != region_b.store.get(k)}
print(f"divergent keys: {len(diff)}")

You'll see hundreds of divergent keys — the classic multi-master conflict. Real systems solve this via (a) sticky routing per key, (b) vector clocks + merge, or (c) CRDTs (grow-only sets, counters).

💡 Hint · Set both region_a.accepts_writes = True AND region_b.accepts_writes = True from the start. Have the client write RANDOMLY to A or B (simulate DNS returning different regions to different clients). Then compare region_a.store vs region_b.store after 2s of writes. You'll see the same key has different values in each region — this is the split-brain problem that requires either 'one region owns writes for each key' or conflict resolution (CRDTs).

(d) Production reality · 15 min

War story AWS us-east-1· 2021Multi-hour degradation; Disney+, Netflix, Coinbase, Roblox all affected
🔥 What broke

An automated scaling event in us-east-1's internal network caused a cascade: EC2 API calls started failing, then Cognito, then Kinesis, then anything that transitively depended on the metadata service. Effectively the entire us-east-1 control plane was down for hours.

Companies without multi-region were fully down. Companies with active-active saw a bump but stayed up. Companies with 'multi-region' that actually meant 'DR in us-east-2 with a manual runbook' spent 3-5 hours failing over — because runbooks that hadn't been tested in 6 months contained stale steps.

🧯 The fix
Two takeaways from every post-mortem that day: (1) test failover monthly, not annually — untested failover is a fantasy, (2) beware 'us-east-1 dependencies you didn't know you had' (AWS IAM, S3 for some global endpoints, Route 53 control plane).
🎓 Lesson to steal
Multi-region is not something you have — it's something you PROVE, monthly, with a real drill. Any team that says 'we have failover' without a monthly test is lying to themselves.
Post-mortem
War story Cloudflare· 201927-minute global outage from a single bad regex
🔥 What broke
Cloudflare pushed a WAF rule containing a regex that caused catastrophic backtracking. Because Cloudflare's edge is Anycast + globally deployed, the bad rule propagated to every POP in seconds and CPUs pegged at 100% globally. Multi-region didn't save them because the CODE was globally deployed synchronously.
🧯 The fix
Staged deployment even for 'safe' config changes: canary → 1% of edge → 10% → 100% over minutes. Simulated regex CPU limits added. Global kill switch for WAF rules.
🎓 Lesson to steal
Multi-region protects against infrastructure failures, not global code deploys. If your code + config ships globally in seconds, a bad deploy is a global outage regardless of how many regions you're in. Add rollout controls at the same level of maturity as failover.
Post-mortem
War story NetflixSuccessful failover during 2017 S3 outage
🔥 What broke
S3 us-east-1 went down for 4 hours. Netflix relies heavily on S3 for content metadata. But Netflix runs active-active across 3 regions with regional caches and content pinning; users didn't notice.
🧯 The fix
No 'fix' needed — it just worked. The design principle: assume any single region can vanish, and design each region to be fully self-sufficient for user-facing paths, with async cross-region replication for the coordination it does need.
🎓 Lesson to steal
The best multi-region outcome looks boring: 'nothing happened, users didn't notice'. The years of investment (Chaos Monkey, regional isolation, active-active) pay off in the one hour that would otherwise be a headline.
Post-mortem

Where this shows up in the rest of the plan

Multi-region touches every layer above and below
S068 · Replication
The mechanism that makes cross-region data possible.
S072 · Consistency
Cross-region always forces a consistency-latency trade.
S077 · Observability
Health checks + latency-per-region are your failover triggers.
S080 · Incident response
Failover IS an incident. Runbooks + gamedays live here.
S089 · Rate limiting
During failover the healthy region gets 2x traffic. Rate limits protect it.
S130 · Design a global service (interview)
The 'design Instagram for 2B users across 3 continents' answer.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

Teach these three, no notes:

  1. RTO vs RPO — one sentence each, plus a real product for each.
  2. The four topologies — cost/time tradeoff.
  3. Why 'active-active' is often marketing — and the one question to ask to catch it.

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.