AWS Networking · 2026-10-17 · Global Accelerator arc

AWS Global Accelerator for MCP Servers: Anycast Routing, Traffic Dials, Health Checks, and Multi-Region Architecture

MCP servers using SSE transport serve long-lived HTTP streams — the TCP handshake and TLS negotiation happen once per session, but that initial latency gap is the first thing a user or agent framework notices; a Tokyo client hitting a us-east-1 MCP endpoint waits 150–200 ms on the public internet before the first SSE byte arrives. AWS Global Accelerator routes that connection through the nearest AWS edge POP over the AWS backbone, cutting first-packet latency by 40–60%; it also issues two permanent static anycast IPs that enterprise firewall teams allowlist once and never revisit, and it reroutes across regions in under 30 seconds without DNS TTL delays. This guide synthesises four GA topics into three structural patterns for MCP teams deploying on ECS Fargate. Pattern 1 covers the GA vs CloudFront vs NLB decision for SSE workloads, the four-object GA resource hierarchy (Accelerator → Listener → EndpointGroup → Endpoint), CDK setup with ClientAffinity.SOURCE_IP, DNS configuration, and pricing — drawing on AWS Global Accelerator for MCP Server Monitoring. Pattern 2 covers listener configuration for MCP SSE, the five traffic dial patterns (single-region, active-passive DR, blue/green, canary 5%, active-active geo split), SOURCE_IP affinity for stateful sessions and its enterprise NAT caveat, per-endpoint weight for in-region canary, proximity routing mechanics at non-100% dials, and standard vs custom routing accelerators for stateful orchestration platforms — from Global Accelerator Routing for MCP Server Endpoints. Pattern 3 covers GA health check architecture (distributed edge probers + majority-vote aggregation), TCP vs HTTP health check selection and the /health endpoint implementation that catches application-layer failures, end-to-end failover timing table (T+0 crash → T+30 GA reroutes → T+300–360 endpoint rejoins pool), layering GA health checks with NLB health checks, blue/green deployment via traffic dial, active-passive vs active-active multi-region architecture, shared session state backends for active-active (DynamoDB Global Tables, ElastiCache Global Datastore, Aurora Global Database), data residency guarantees, and cost model — synthesised from Global Accelerator Health Checks for MCP Server Endpoints and Multi-Region MCP Servers with Global Accelerator.

TL;DR

Pattern 1 — GA Architecture, CDK Setup, and the GA vs CloudFront vs NLB Decision

Why the GA vs CloudFront vs NLB decision matters for MCP

MCP's SSE transport is a long-lived HTTP GET where the server streams newline-delimited JSON events into an open connection. A client holds this connection for the duration of an agent session — potentially for hours. The connection setup happens once: TCP handshake, TLS negotiation, the MCP initialize exchange. After that, the channel is open and events flow as they arrive. This connection lifetime is what determines which AWS networking layer belongs in front of your MCP server.

CloudFront is a content delivery network. Its fundamental model is: cache a response at a POP, serve it to subsequent clients without hitting the origin. SSE is the opposite of cacheable — every client needs an independent, origin-connected stream. CloudFront has a fixed 30-second connection timeout that kills quiet SSE streams silently; there is no CloudFront configuration option to extend this timeout for streaming responses. CloudFront is the wrong choice as the primary layer for MCP SSE transport. (It's fine for serving the MCP server's static assets, documentation pages, or non-streaming API endpoints.)

NLB is the right choice for single-region MCP deployments. Its 350-second TCP idle timeout is fixed at the platform level — you cannot accidentally misconfigure it. It assigns static Elastic IPs per AZ that enterprise firewall teams can allowlist without chasing DNS. It supports TCP pass-through for mTLS client certificate authentication. And it does not terminate SSE connections. For a team serving MCP clients in one region, NLB is sufficient and simpler. See AWS NLB for MCP Servers for the full NLB guide.

Global Accelerator adds value when single-region NLB is not enough. Three specific triggers: multi-geographic latency (Tokyo clients connecting to us-east-1 experience 150–200 ms round trips on the public internet before the first SSE byte; GA routes that initial handshake through the nearest AWS edge POP and then over the AWS backbone, cutting first-packet latency by 40–60%), static IPs for enterprise firewall policy (GA's two permanent anycast IPs survive NLB replacements, region migrations, and infrastructure changes behind the accelerator — enterprise clients configure their egress firewall once), and cross-region failover without DNS TTL delays (Route 53 TTL-based failover takes 60–180 seconds for DNS caches to expire; GA detects endpoint failure and reroutes in under 30 seconds at the TCP routing level).

The 10-property GA vs CloudFront vs NLB comparison table

Every property in this table corresponds to a concrete MCP deployment decision:

PropertyGlobal AcceleratorCloudFrontNLB (single-region)
OSI layer L3/L4 (TCP/UDP) L7 (HTTP/HTTPS) L4 (TCP/UDP/TLS)
SSE / WebSocket Transparent TCP pass-through — streams pass unchanged SSE not supported (30s timeout kills stream); WebSocket supported Transparent TCP pass-through
Static IPs 2 permanent anycast IPs per accelerator — survive NLB replacement No — CloudFront IP set changes 1 Elastic IP per AZ — static but not anycast
Multi-region routing Yes — anycast to nearest AWS POP, backbone to origin region Yes — CDN cache distribution No — single region
Cross-region failover speed ~30 seconds at TCP routing layer ~60 seconds (origin failover group) Not applicable
WAF integration No L7 WAF at GA; attach WAF to ALB behind GA Yes — AWS WAF at edge No
Caching None — pure routing Full CDN cache None — pure routing
Idle timeout Controlled by target (NLB 350s, ALB configurable) 30 seconds fixed — kills SSE connections 350 seconds fixed
Pricing $0.025/hr per accelerator + data transfer premium $0.0085/10K requests + data transfer $0.008/hr per LCU
Best for MCP Multi-region SSE endpoints or enterprise static IP requirements Static assets, docs, API responses only (not SSE streams) Single-region SSE endpoints, mTLS, high throughput

The decision rule: CloudFront for caching and edge WAF (never for MCP SSE streams). NLB for single-region MCP. Global Accelerator when you need geographic latency reduction, global static IPs, or cross-region failover. In practice, GA sits in front of regional NLBs — the architecture is GA → NLB → ECS Fargate tasks.

GA resource hierarchy: four objects

A Global Accelerator deployment is modelled as four nested objects:

For a two-region MCP deployment, you create one accelerator, one listener (port 443, SOURCE_IP affinity), two endpoint groups (us-east-1 at 100% dial, eu-west-1 at 100% dial), and one NLB endpoint per group.

CDK: Global Accelerator in front of a regional NLB

Both CDK packages required ship with CDK v2 — no additional install:

import * as ga from 'aws-cdk-lib/aws-globalaccelerator';
import * as gaEndpoints from 'aws-cdk-lib/aws-globalaccelerator-endpoints';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import { CfnOutput, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export function createMcpGlobalAccelerator(
  scope: Construct,
  nlb: elbv2.NetworkLoadBalancer,
  region: string,
): ga.Accelerator {

  const accelerator = new ga.Accelerator(scope, 'McpAccelerator', {
    acceleratorName: 'alivemcp-accelerator',
    enabled: true,
  });

  const listener = accelerator.addListener('McpListener', {
    portRanges: [{ fromPort: 443, toPort: 443 }],
    protocol: ga.ConnectionProtocol.TCP,
    // SOURCE_IP affinity: same client IP always hashes to same endpoint.
    // Required for stateful SSE sessions — prevents mid-session re-routing
    // to a different NLB which would drop the TCP connection.
    clientAffinity: ga.ClientAffinity.SOURCE_IP,
  });

  listener.addEndpointGroup('PrimaryGroup', {
    region,
    trafficDialPercentage: 100,
    // TCP health check: fastest, but blind to application-layer failures.
    // Switch to HTTPS + /health path to catch 500s and broken DB pools.
    healthCheckProtocol: ga.HealthCheckProtocol.TCP,
    healthCheckInterval: ga.HealthCheckInterval.HEALTH_CHECK_INTERVAL_10,
    healthCheckThreshold: 3,
    endpoints: [
      new gaEndpoints.NetworkLoadBalancerEndpoint(nlb, {
        weight: 128,
        // Preserve real client IP through GA → NLB → ECS.
        // Requires NLB preserveClientIp: true AND task SG
        // inbound from 0.0.0.0/0 (not just NLB SG).
        preserveClientIp: true,
      }),
    ],
  });

  // These two IPs never change — export them for firewall allowlisting.
  new CfnOutput(scope, 'AcceleratorIp1', {
    value: accelerator.ipv4Addresses[0],
    description: 'GA static IP #1 — add to enterprise firewall allowlist',
  });
  new CfnOutput(scope, 'AcceleratorIp2', {
    value: accelerator.ipv4Addresses[1],
    description: 'GA static IP #2 — add to enterprise firewall allowlist',
  });

  return accelerator;
}

The two IPs in the CloudFormation outputs are the enterprise firewall allowlist entries. When you replace the NLB during a Fargate migration or an Availability Zone change, the IPs in those outputs remain the same. Enterprise clients do not need to update their firewall rules.

DNS setup: CNAME vs Route 53 alias

GA assigns a hostname like a1b2c3d4e5f6.awsglobalaccelerator.com. To serve MCP at your own domain:

If your domain uses Route 53, always prefer the alias record. If using an external registrar (as AliveMCP does for alivemcp.com on Spaceship), CNAME is the only option and works correctly.

How anycast routing reduces MCP connection latency

When GA is active, both its IPv4 addresses are announced via BGP from every AWS edge POP simultaneously — roughly 100 locations worldwide. A client's TCP SYN packet routes to the nearest AWS POP instead of all the way to your origin region. The POP accepts the TCP handshake locally, then carries the session over the AWS private backbone to your NLB.

For MCP servers, the benefit concentrates at connection setup. Each SSE session requires a TCP handshake and two TLS round trips before the first MCP event arrives. For a Tokyo client connecting to us-east-1:

After the SSE connection is established, GA is a transparent TCP proxy. Event delivery latency is determined by the backbone and the origin region, not by GA. The latency saving is front-loaded at connection time, which matters for interactive agentic workflows where the first tool call latency sets the user's perceived responsiveness.

Pricing: when GA is cost-justified for a commercial MCP product

GA has two cost components:

ComponentRateMonthly estimate (10 GB transferred)
Accelerator fixed charge $0.025/hour $18.00 (730 hours)
Data transfer premium (US origin, inter-region) $0.035/GB US→EU ~$0.35 (10 GB at mixed rates)
Total (one accelerator, 10 GB/mo) — ~$18.35/month

The $18/month fixed charge dominates for typical MCP servers — SSE streams carry text events at low bandwidth. The data transfer premium is a rounding error at the scale of a $9/mo Author tier product. GA makes sense once you have enterprise Team tier customers ($49/mo each) who require static IPs for firewall policy or sub-30-second cross-region failover SLAs. For hobby MCP servers serving a single geographic region, GA is overkill.

AliveMCP uses GA for its probe coordinator: eight probe agents in eight AWS regions need a stable entry point to deliver probe results back to the coordinator in us-east-1. With GA, probe agents are configured with two hardcoded static IPs. When AliveMCP replaced the coordinator NLB during a Fargate migration, the GA IPs never changed — all eight probe agents continued delivering through the maintenance window without reconfiguration.

Pattern 2 — Routing, Traffic Dials, CLIENT_AFFINITY, and Custom Routing Accelerators

Listener configuration: one SSE listener + one management listener

For most MCP deployments you want two listeners: one for the SSE stream (stateful, needs affinity) and one for the management API (stateless, no affinity needed):

// Listener for MCP SSE (HTTPS/443) — SOURCE_IP affinity required
const mcpListener = accelerator.addListener('McpSseListener', {
  portRanges: [{ fromPort: 443, toPort: 443 }],
  protocol: ga.ConnectionProtocol.TCP,
  clientAffinity: ga.ClientAffinity.SOURCE_IP,
});

// Separate listener for MCP management API (8443) — stateless, no affinity
const mgmtListener = accelerator.addListener('McpMgmtListener', {
  portRanges: [{ fromPort: 8443, toPort: 8443 }],
  protocol: ga.ConnectionProtocol.TCP,
  clientAffinity: ga.ClientAffinity.NONE,
});

SOURCE_IP affinity is a consistent hash on the client's source IP address. When a client's SSE connection drops and reconnects from the same IP, the hash resolves to the same endpoint group and the same backend NLB — session continuity is preserved through reconnects.

Enterprise NAT caveat: clients behind a corporate NAT share one outbound IP. If 500 corporate users all appear as 203.0.113.1, they all hash to the same endpoint group and potentially the same backend task. SOURCE_IP affinity is a session-continuity mechanism, not a load balancing mechanism. Provision capacity assuming the worst-case NAT IP concentration in your user base, and use endpoint weights within a group (not SOURCE_IP) for load distribution.

The five traffic dial patterns

Traffic dials (0–100% per endpoint group) control what fraction of total traffic each region receives. GA distributes proportionally by dial, modified by proximity. Five patterns cover the standard deployment scenarios:

PatternPrimary group dialSecondary group dialUse case
Single region (default) 100% — Startup / single-region deployment; simplest configuration
Active-passive DR 100% 0% DR region warm and health-checked but receiving zero live traffic; failover by swapping dials
Blue/green deployment 100% → 0% (blue) 0% → 100% (green) Ramp green region from 0%; smoke test; full cut; decommission blue
Canary 5% 95% 5% 5% of all traffic routes to a new region or version; validate before full ramp
Active-active (geo split) 100% 100% Both regions fully live; GA proximity routing handles geographic split; requires shared session state

Important nuance on active-active: traffic dials interact with proximity in a non-obvious way. With both groups at 100%, GA routes each client to the geographically nearest healthy group — a London client goes to eu-west-1, a New York client goes to us-east-1. This is geographic load splitting without explicit per-client routing rules. If you set eu-west-1 to 30%, GA routes approximately 30% of global clients to eu-west-1 regardless of whether those clients are geographically close to eu-west-1. The proximity algorithm is only the deciding factor when all groups are at 100%. For true latency-based geographic routing, set all active groups to 100% and let GA's proximity model do the work.

Proximity-based routing: when GA picks the nearest group

GA's proximity model is based on measured latency from client ingress POPs to endpoint groups, not physical distance. The practical effects:

Per-endpoint weights for in-region canary deployments

Within a single endpoint group, weights (0–255) enable in-region version canary without touching the traffic dial:

listener.addEndpointGroup('UsEast1Group', {
  region: 'us-east-1',
  trafficDialPercentage: 100,
  endpoints: [
    // Existing NLB (current MCP version) — 95% of weight-proportional traffic
    new gaEndpoints.NetworkLoadBalancerEndpoint(existingNlb, {
      weight: 100,
      preserveClientIp: true,
    }),
    // New NLB (new MCP version) — 5% of weight-proportional traffic
    // Increase this weight as validation passes; remove old NLB when done.
    new gaEndpoints.NetworkLoadBalancerEndpoint(newNlb, {
      weight: 5,
      preserveClientIp: true,
    }),
  ],
});

Setting weight to 0 drains an endpoint without removing it from the group configuration — useful for graceful maintenance. The combination of traffic dial (which region) and per-endpoint weight (which backend within a region) gives fine-grained rollout control at every granularity.

Standard vs custom routing accelerators

Standard accelerators (the default) let GA decide which endpoint receives each connection based on health and weight. For most MCP servers — stateless tool servers where any backend task can handle any client — this is the right choice.

Custom routing accelerators work differently: you define a port range on the listener (e.g., 10000–20000), and GA deterministically maps each (accelerator_ip, port) combination to exactly one (ec2_instance_ip, port). Every connection to 1.2.3.4:10042 always goes to 10.0.0.5:8042, and no load balancing decision is made.

PropertyStandard acceleratorCustom routing accelerator
Endpoint selection GA selects based on health + weight Deterministic: each IP:port maps to one EC2 instance:port
Load balancing Yes — GA distributes across healthy endpoints No — you manage the port:instance mapping externally
Target type NLB, ALB, EC2, Elastic IP EC2 instance only
Session affinity SOURCE_IP hash (optional) — breaks if client IP changes Guaranteed — same port always reaches same instance regardless of client IP
MCP use case Standard SSE endpoints with stateless tool servers or SOURCE_IP-stable clients Agent orchestration platforms where each MCP session must bind to a specific process

Custom routing is for MCP orchestration platforms that implement their own connection mapping layer — where each orchestrator process owns a set of downstream tool-server connections that cannot migrate to a different process without session teardown. For standard MCP tool servers, standard accelerator + SOURCE_IP affinity is sufficient and significantly simpler to operate.

GA and NLB health check layering: a blind spot to know

GA health-checks the NLB endpoint directly; NLB health-checks the ECS tasks behind it. These are two independent detection planes with different blind spots:

Pattern 3 — Health Checks, Failover Timing, and Multi-Region Architecture

GA health check architecture: distributed probers and majority vote

Unlike Route 53 health checks (8–15 fixed probe IPs) or NLB health checks (probe from NLB nodes per AZ), GA health checks are issued from multiple AWS edge locations distributed globally. An endpoint transitions to unhealthy only when a majority of probing locations report failure for 3 consecutive intervals — transient single-location failures do not trigger failover.

Health check configuration lives at the endpoint group level. For production MCP endpoints:

listener.addEndpointGroup('UsEast1Group', {
  region: 'us-east-1',
  trafficDialPercentage: 100,

  // HTTPS health check: catches application-layer failures
  // TCP check passes even when MCP app returns 500s or hangs
  healthCheckProtocol: ga.HealthCheckProtocol.HTTPS,

  // Port for GA health check — separate from main listener port.
  // If your main listener is NLB TCP pass-through (TLS-only port 443),
  // use a plaintext /health endpoint on port 8080 here.
  healthCheckPort: 8080,

  // 10s interval: detection window = 3 failures × 10s = 30s
  // Default 30s interval: detection window = 3 × 30s = 90s
  healthCheckInterval: ga.HealthCheckInterval.HEALTH_CHECK_INTERVAL_10,

  // Consecutive failures before marking unhealthy; consecutive successes
  // before marking healthy again. Default and minimum: 3.
  healthCheckThreshold: 3,
  // Note: healthCheckPath ('/health') is a CfnEndpointGroup L1 property:
  // (cfnGroup as ga.CfnEndpointGroup).healthCheckPath = '/health';

  endpoints: [
    new gaEndpoints.NetworkLoadBalancerEndpoint(nlb, { weight: 128 }),
  ],
});

TCP vs HTTP health checks: why it matters for MCP

TCP health checks verify only that the port accepts a TCP connection. Your MCP server will pass a TCP check even when:

HTTP health checks fix this by requiring your server to return 200 to a specific GET request. Implement /health to check the conditions that matter for MCP protocol correctness:

app.get('/health', async (req, res) => {
  const dbOk = await checkDatabase();         // SELECT 1 or ping
  const toolsOk = toolRegistry.size > 0;      // tool list loaded
  const loopOk = Date.now() - lastTick < 5000; // event loop not stalled

  if (dbOk && toolsOk && loopOk) {
    res.status(200).json({ status: 'ok', uptime: process.uptime() });
  } else {
    res.status(503).json({ db: dbOk, tools: toolsOk, loop: loopOk });
  }
});

Sharp edge for NLB TCP pass-through: if your main listener is NLB in TCP pass-through mode (port 443 carries raw TLS that the container terminates), GA cannot run HTTP/HTTPS health checks on port 443 — it can't terminate TLS to issue an HTTP GET. Solution: expose a plaintext HTTP listener on port 8080 that the GA health check targets, bound to the VPC CIDR so it's not internet-accessible.

End-to-end failover timing walkthrough

At 10-second health check interval with threshold 3:

T (seconds)Event
0 MCP server process crashes; port 8080 /health returns 503 or times out
0–10 GA issues first health check; receives failure. Failure count: 1/3.
10–20 GA issues second health check; still failing. Failure count: 2/3.
20–30 GA issues third health check; still failing. Endpoint marked unhealthy.
~30 GA starts routing new connections to the next healthy endpoint group (second region or second endpoint in same group).
30+ Existing SSE connections to the failed endpoint are dropped (TCP RST from NLB as ECS tasks drain). MCP clients reconnect; GA routes them to the healthy endpoint.
30–300 ECS service replaces the crashed task (Fargate task launch: 30–90s); NLB health check passes; NLB marks new task healthy (NLB timing: ~20s at default interval).
300–360 GA health checks the NLB with healthy task; 3 consecutive successes at 10s = 30s. GA re-includes endpoint in routing pool.

Total mean time to recovery for new connections: ~35 seconds from crash (GA reroutes within the 30s detection window). For pre-warmed second endpoint or DR region at 0% dial, failover is instantaneous once GA marks the primary unhealthy — GA can reroute to a 0%-dial group immediately by changing the dial to 100% via a Lambda triggered by the health check state change. Without a warm standby, the failed endpoint's replacement is not back in the GA pool until T+300–360s (ECS replacement + NLB registration + GA re-check).

Layering GA health checks with NLB health checks: failure modes table

Failure typeGA detects?NLB detects?Outcome
All ECS tasks crash Yes — NLB returns no healthy targets; GA HTTP check fails Yes — tasks deregistered GA fails over after 30s; NLB stops routing within 20s
One of three tasks crashes No — other tasks healthy; NLB endpoint still passes Yes — crashed task deregistered; remaining tasks still serve Reduced capacity; no GA failover; NLB handles gracefully
MCP app returning 500s (port open) Yes — if using HTTP GA check on /health Yes — if NLB also checks /health Both detect; NLB drains tasks; GA fails over
Full region AWS outage Yes — NLB endpoints fail across AZs Partial — AZ-specific tasks fail GA fails over to second region within 30s
NLB misconfigured (wrong port) Yes — GA HTTP check to NLB fails immediately N/A — NLB config error GA failover triggers; fix misconfiguration

The remaining blind spot at both layers: if your MCP server is healthy from a TCP/HTTP perspective but returning errors for specific tool calls (a broken tool handler that throws for one of ten tools), neither GA nor NLB detects this. AliveMCP addresses this by issuing live MCP protocol probes — actual tools/list and sample tool call invocations — not just TCP and HTTP health checks. This is what the AliveMCP probe does for every registered endpoint on its 60-second cadence.

Blue/green deployments with GA traffic dial

The safest pattern for rolling out a new MCP server version combines GA traffic dial control with health checks that block routing to an unhealthy green region:

// Step 1: Green endpoint group starts at 0% traffic dial.
// GA health-checks green continuously but routes zero live traffic.
const greenGroup = listener.addEndpointGroup('GreenGroup', {
  region: 'us-east-1',
  trafficDialPercentage: 0,
  healthCheckProtocol: ga.HealthCheckProtocol.HTTPS,
  healthCheckPort: 8080,
  healthCheckInterval: ga.HealthCheckInterval.HEALTH_CHECK_INTERVAL_10,
  healthCheckThreshold: 3,
  endpoints: [
    new gaEndpoints.NetworkLoadBalancerEndpoint(greenNlb, { weight: 128 }),
  ],
});

// Step 2: Deploy new MCP version to green ECS service.
// Step 3: Wait for GA HealthyEndpointCount == 1 for GreenGroup.
// Step 4: Smoke-test green directly (bypass GA; connect to NLB DNS name).
// Step 5: Ramp green to 10% via UpdateEndpointGroup API.
// Step 6: Monitor error rate for 5 minutes. Roll back at any sign of trouble.
// Step 7: Ramp green to 100%; set blue to 0%.
// Step 8: Wait for blue ActiveFlowCount → 0 (all SSE sessions migrated or expired).
// Step 9: Delete blue endpoint group + decommission blue ECS service.

Critical timing detail for SSE: setting blue dial to 0% stops new connections from routing to blue, but does not terminate existing SSE connections. Long-running agentic sessions may stay connected to blue for minutes to hours. Only after blue's NLB ActiveFlowCount approaches 0 should you decommission the blue ECS service. Decommissioning while SSE clients are still connected to blue produces visible disconnects from the user's perspective.

Multi-region architecture: active-passive vs active-active

Multi-region is justified when one or more of these triggers applies:

For most indie MCP server authors at AliveMCP's Author tier, multi-region is premature. A single us-east-1 NLB behind GA for static IPs handles the first 1,000 users. Multi-region becomes relevant when Team and Enterprise tier customers with SLA commitments or data residency requirements arrive.

Active-passive: warm DR and traffic dial failover

Active-passive is the simpler architecture. Primary region at 100% dial handles all traffic. DR region at 0% dial is deployed, health-checked, and ready — but receives nothing until the primary fails. A Lambda triggered by a CloudWatch alarm flips the dials:

async function failoverToDr() {
  const client = new GlobalAcceleratorClient({ region: 'us-east-1' });
  await client.send(new UpdateEndpointGroupCommand({
    EndpointGroupArn: primaryGroupArn,
    TrafficDialPercentage: 0,
  }));
  await client.send(new UpdateEndpointGroupCommand({
    EndpointGroupArn: drGroupArn,
    TrafficDialPercentage: 100,
  }));
}

DR region warm-up strategy: keep 1–2 ECS tasks running in the DR region at all times. Fargate cold start is 30–90 seconds. If DR has zero running tasks when failover triggers, new connections from GA fail while ECS scales up. The cost of two warm standby tasks ($20–40/month on Graviton ARM64) is less than the downtime cost of a 90-second gap in failover coverage.

Active-passive avoids cross-region session state sync. DR has its own local database. In-flight sessions are lost on failover, but new sessions start cleanly on DR's local storage. For stateless MCP tool servers (no server-side session memory between calls), this is transparent to users.

Active-active: shared session state requirements

Active-active sends live traffic to all endpoint groups simultaneously. GA proximity routing handles geographic split automatically (London → eu-west-1, New York → us-east-1). The challenge is that MCP sessions are stateful: an SSE connection has a session ID; if a client reconnects after a disconnect and lands in a different region, that region has no knowledge of the session's tool context.

Shared session state backends for active-active MCP:

BackendApproachCross-region write latencyConsistency
DynamoDB Global Tables Session state written to DynamoDB; Global Tables replicate to all active regions in <1s ~100ms async replication (eventual) Eventual — last-writer-wins conflict resolution
ElastiCache Global Datastore (Redis) Primary Redis cluster in one region; read-only replicas elsewhere; writes always go to primary ~50ms replica lag; writes to primary always Strong for writes (primary); eventual for replica reads
Aurora Global Database Aurora primary + read replicas; writes cross-region to primary ~30ms write lag; reads from local replica Strong (primary reads); eventual (replica reads)
Stateless MCP server Each tool call carries full context in request body; server holds no session memory N/A — no sync needed N/A

The stateless MCP server pattern is the cleanest architecture for active-active multi-region: design each tool call to carry all required context in the request body rather than relying on server-side session memory between calls. The SSE connection is maintained for event delivery, but each tool call is independently resolvable. This matches the MCP protocol's design intent and makes cross-region failover transparent to the MCP client.

Data residency: hard vs soft guarantees

For GDPR or EU AI Act compliance, tool call data including personal information must not leave the EU. GA proximity routing typically sends London clients to eu-west-1 — but "typically" is not a compliance guarantee. To harden data residency:

  1. SOURCE_IP affinity: ensures reconnections from the same IP route to the same region as long as that region is healthy. Soft guarantee — fails over to us-east-1 if eu-west-1 becomes entirely unavailable.
  2. Two-accelerator architecture (hard guarantee): deploy two separate GAs — one EU GA with only the eu-west-1 endpoint group, one global GA with only the us-east-1 endpoint group. Use Route 53 geolocation routing to send EU-resolved domain names to the EU GA. EU clients never connect to a GA that has a non-EU endpoint. Cost: +$18/month for the second accelerator — trivial relative to the compliance risk of single-accelerator soft guarantees.

Consult legal before choosing between soft and hard data residency guarantees based on your data sensitivity and regulatory context.

Multi-region cost model

ComponentActive-passive cost deltaActive-active cost delta
GA accelerator +$18/month +$18/month
Secondary NLB +$16/month (near-zero LCUs at 0% dial) +$16/month/additional region (full LCU cost)
DR/secondary ECS tasks (warm) +$20–40/month (1–2 Graviton Fargate tasks) Full capacity cost per additional region
DynamoDB Global Tables replication N/A (active-passive doesn't sync) +$0.10/million replicated writes (usually <$5/month for most MCP servers)
GA data transfer premium +$0.035/GB inter-region +$0.035/GB inter-region

For a commercial MCP server at AliveMCP's Team tier ($49/month per customer), active-passive multi-region infrastructure adds roughly $70–90/month in AWS costs. This cost is justified when you have approximately 5 Team-tier customers who have expressed data residency or sub-5-minute RTO SLA requirements. For Enterprise tier ($299/month), multi-region is table stakes and the infrastructure cost is a small fraction of revenue per customer.

How AliveMCP monitors Global Accelerator-fronted MCP endpoints

AliveMCP's probe fleet runs in 8 AWS regions. For MCP servers deployed behind GA, we probe at two independent layers simultaneously:

When origin is unhealthy but the GA endpoint appears healthy, AliveMCP flags the server as "GA-masked partial failure" — GA is routing around the degraded origin, but recovery depends on the failed origin returning to service. We notify the server author via webhook (Author tier) because users will experience disruption when GA's failover capacity is exhausted or the failed origin affects a GA-configured DR region. When the GA endpoint is unhealthy but origins are healthy, we flag "GA routing failure" — the routing layer itself is misconfigured, not the MCP server, and the fix is a GA configuration change rather than an application deployment.