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
- GA vs CloudFront vs NLB decision and CDK setup: Use NLB for single-region MCP SSE deployments needing static IPs or mTLS. Use CloudFront only when you need content caching or edge WAF — CloudFront's fixed 30-second connection timeout kills SSE connections silently, making it unsuitable as the primary layer for SSE streams. Use Global Accelerator when your MCP server serves users in multiple geographic regions (anycast routing eliminates the public internet path from client to nearest AWS POP), when enterprise clients need two stable anycast IPs for firewall allowlisting that survive infrastructure replacements behind GA, or when you need cross-region failover in under 30 seconds without Route 53 TTL delays. GA is L3/L4 — SSE and WebSocket pass through unchanged. Fixed cost: $18/month per accelerator plus a small data transfer premium. CDK: import from
aws-cdk-lib/aws-globalaccelerator(Accelerator, Listener, EndpointGroup) andaws-cdk-lib/aws-globalaccelerator-endpoints(NetworkLoadBalancerEndpoint wrapper). Create the accelerator, add a listener withportRanges: [{ fromPort: 443 }], setclientAffinity: ga.ClientAffinity.SOURCE_IPfor SSE stickiness, add an endpoint group for your region withtrafficDialPercentage: 100and TCP health checks, attach the NLB vianew gaEndpoints.NetworkLoadBalancerEndpoint(nlb, { weight: 128, preserveClientIp: true }). CloudFormation outputs export both static anycast IPs — they never change even when you replace the NLB. DNS: CNAME to the GA hostname (xxxx.awsglobalaccelerator.com) for external DNS providers; Route 53 alias record for zero-cost zone-apex mapping. Key metrics inAWS/GlobalAcceleratornamespace:NewFlowCount(alert if drops to 0 during expected traffic hours — DNS or routing failure),ProcessedBytesIn/Out(alert on >50% sudden drop — endpoint failure),HealthyEndpointCount(alert when <1 in any group),UnhealthyEndpointCount(alert if >0). Combine GANewFlowCountwith NLBActiveFlowCount— they measure the same connections at different layers and should track together; a spike in GANewFlowCountwithout matching NLB spike indicates a routing discrepancy. - Routing, traffic dials, SOURCE_IP affinity, and custom routing: Set client affinity to SOURCE_IP on every listener carrying SSE sessions — SOURCE_IP is a consistent hash on the client's source IP address, not the full 5-tuple; a client reconnecting from the same IP after a dropped SSE connection lands on the same endpoint group and the same backend. Without it, reconnecting clients may hit a different region and lose session context. SOURCE_IP affinity caveat: clients behind corporate NAT share one outbound IP — all of them hash to the same endpoint, potentially routing 500 corporate users to a single ECS task. Provision capacity accordingly; don't rely on SOURCE_IP for load distribution. Traffic dials (0–100% per endpoint group) are the primary knob for blue/green and canary deployments. Five patterns: single-region at 100%; active-passive DR at 100% primary / 0% secondary (warm DR region health-checked but receives no traffic, failover by swapping dials via Lambda triggered by CloudWatch alarm); blue/green by slowly ramping green from 0% to 100% while blue drops from 100% to 0%; canary at 95%/5% to validate a new version or new region; active-active at 100%/100% where GA proximity routing sends each client to the nearest healthy group. When multiple groups are at 100% GA routes by geographic proximity — London clients go to eu-west-1, New York clients to us-east-1. Dials override proximity: a group at 0% receives no traffic regardless of geographic proximity; a group at 30% receives roughly 30% of global clients regardless of whether those clients are geographically nearby, which is why active-active works best with all groups at 100% so that geographic split is handled entirely by GA's proximity algorithm. Per-endpoint weights (0–255) within a group enable in-region canary: set the existing NLB at weight 100 and a new NLB at weight 5; validate; increase new NLB weight while decreasing old; remove old when done. Setting weight to 0 drains an endpoint without deleting it — useful for graceful maintenance. Custom routing accelerators assign a deterministic
(accelerator_ip, port)→(ec2_instance_ip, port)mapping with no load balancing decision; every connection to1.2.3.4:10042always goes to10.0.0.5:8042. This is for agent orchestration platforms where each client session must bind to a specific MCP server process — standard accelerator + SOURCE_IP affinity breaks if a client's IP changes (mobile, VPN reconnect), whereas custom routing guarantees the mapping regardless of source IP. - Health checks, failover timing, multi-region architecture, and cost model: GA health checks are issued from multiple AWS edge locations simultaneously (not a single prober), aggregated by majority vote — a transient single-region probe failure does not trigger failover. Default check interval is 30s; set it to 10s for production MCP endpoints. Threshold: 3 consecutive failures to mark unhealthy, 3 consecutive successes to recover. Total detection window at 10s interval: 30 seconds. Use HTTP health checks on a dedicated plaintext port (8080) pointing to
/health— TCP health checks pass silently when your MCP process is returning 500s, the event loop is hung, or the database connection pool is exhausted. The/healthendpoint should check: DB ping (SELECT 1), tool registry loaded (non-empty tool list), and event loop lag (<5s since last tick). If all pass, return 200; otherwise 503. Sharp edge: if your main listener is NLB TCP pass-through (TLS-only port 443), GA cannot run HTTPS health checks on that port; expose a separate plaintext listener on port 8080 bound to VPC CIDR only for health checks. Failover timing at 10s interval: T+0 crash → T+30 GA marks endpoint unhealthy and reroutes new connections → T+30–300 ECS service replaces the failed task and NLB registers it → T+300–360 GA health checks the new NLB (3 × 10s = 30s) and re-includes endpoint. Total mean time to recovery for new connections: ~35s from crash. Pre-warm a second endpoint or region at 0% traffic dial for sub-5-minute full recovery — ECS task replacement takes 30–60s, and without a warm standby the failed endpoint's replacement is off the GA pool until it passes health checks. Layering: GA checks the NLB endpoint; NLB checks ECS tasks. Failure blind spot: if all ECS tasks crash, NLB returns no healthy targets → GA HTTP check to NLB fails → GA reroutes in 30s. But if one of three tasks crashes (partial failure), NLB still has healthy tasks → GA sees a healthy NLB → no GA failover, reduced capacity handled by NLB alone. Active-passive vs active-active: active-passive keeps DR at 0% dial, avoiding cross-region state sync (DR has its own local SQLite/RDS, session state is lost on failover but new sessions start cleanly on DR). Active-active sends live traffic to all regions; clients reconnecting after failover may land in a different region — requires shared session state via DynamoDB Global Tables (~100ms async replication, last-writer-wins) or ElastiCache Global Datastore (~50ms replica lag, writes always go to primary region). The stateless MCP server pattern avoids cross-region state sync entirely: each tool call carries full context in the request body, the server holds no session memory between calls. Data residency hard guarantee: use two separate accelerators (EU GA with only eu-west-1 endpoint, global GA with us-east-1) + Route 53 geolocation routing to direct EU-domain requests to the EU GA; single-accelerator SOURCE_IP affinity provides only a soft guarantee because GA fails over to us-east-1 if eu-west-1 is entirely unavailable. Multi-region cost delta: +$18/mo GA fixed + $16/mo secondary NLB + $20–40/mo warm DR ECS tasks (active-passive) or full secondary capacity (active-active). Justified at roughly 5 Team-tier customers ($49/mo each) who require data residency or sub-5-minute RTO SLAs.
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:
| Property | Global Accelerator | CloudFront | NLB (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:
- Accelerator — the top-level resource. AWS announces two static anycast IPv4 addresses (and optionally one IPv6 address) from every AWS edge POP simultaneously. A DNS hostname like
a1b2c3d4e5f6.awsglobalaccelerator.comresolves to these IPs. They never change for the lifetime of the accelerator, even when the backend NLBs are replaced. - Listener — attached to the accelerator. Specifies: protocol (TCP or UDP), port range (e.g., 443–443 or 8080–8080), and client affinity (NONE or SOURCE_IP). One accelerator can have multiple listeners on different ports.
- Endpoint group — scoped to a single AWS region. Each listener can have one endpoint group per region. Contains: traffic dial (0–100%), health check settings (protocol, interval, threshold, path), and one or more endpoint references.
- Endpoint — the actual target: NLB, ALB, EC2 instance, or Elastic IP. Each endpoint has a weight (0–255). Setting weight to zero removes the endpoint from traffic distribution without deleting it.
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:
- CNAME — for external DNS providers (Spaceship, Cloudflare, etc.). Add a CNAME from
mcp.yourcompany.comto the GA hostname. TTL recommendation: 60–300s. The CNAME resolves to the GA anycast IPs. Note: CNAMEs cannot be used at zone apex (yourcompany.com). - Route 53 alias — zero-cost DNS queries, works at zone apex, no CNAME chain overhead. In CDK:
new route53.ARecord({ recordName: 'mcp', target: route53.RecordTarget.fromAlias(new targets.GlobalAcceleratorTarget(accelerator)) }). The alias record resolves directly to the GA anycast IPs.
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:
- Without GA: ~170 ms round trip Tokyo → US East; TLS at 2 RTTs = ~510 ms before first byte.
- With GA: ~3 ms Tokyo → Tokyo POP; TCP handshake at the POP then backbone to us-east-1; effective first-byte time ~250 ms — roughly 50% faster connection setup.
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:
| Component | Rate | Monthly 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:
| Pattern | Primary group dial | Secondary group dial | Use 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:
- A London client with groups in us-east-1 (100%) and eu-west-1 (100%) typically routes to eu-west-1 — London POP has lower backbone latency to eu-west-1 than to us-east-1.
- If eu-west-1 endpoints go unhealthy, GA fails over London clients to us-east-1 within 30 seconds — no DNS TTL to wait out.
- Setting eu-west-1 to 0% for planned maintenance sends all London clients to us-east-1 regardless of proximity. Set it back to 100% when maintenance ends.
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.
| Property | Standard accelerator | Custom 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:
- NLB healthy, MCP app returning 500s: if the NLB health check only checks TCP connectivity (port open) and the MCP process is returning 500s for every tool call, GA sees a healthy NLB and keeps routing traffic to it. Fix: use HTTP health checks at both NLB and GA that verify the
/healthendpoint returns 200. - All ECS tasks crash: NLB deregisters all tasks (unhealthy). GA's HTTP check to NLB fails (no healthy targets → NLB returns TCP RST or 503). GA marks endpoint unhealthy and reroutes to the next group within 30 seconds.
- GA endpoint healthy, but origin partially degraded: if GA routes around a partial failure (one of two regions down), GA reports healthy from the client's perspective while the remaining region runs at double load. AliveMCP probes both GA endpoints and origin endpoints separately, flagging this as a "GA-masked partial failure" — GA is currently routing around the failure, but recovery depends on the failed origin coming back.
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:
- The MCP process is responding to all tool calls with HTTP 500 (crash-loop survivor state)
- The event loop is blocked and not sending the initial SSE event
- The database connection pool is exhausted (tool calls fail, but TCP open)
- The tool registry failed to load (port accepts connections but
tools/listreturns an error)
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 type | GA 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:
- First-byte latency for a significant user segment exceeds 150 ms (add an endpoint group in the nearest region)
- Data residency rules require EU client data to stay in EU (active-active with eu-west-1 group + SOURCE_IP affinity)
- RTO SLA requires full region failure recovery in under 5 minutes (active-passive with warm DR)
- Concurrent SSE connections exceed single-region NLB capacity (>1M concurrent — distribute via traffic dials)
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:
| Backend | Approach | Cross-region write latency | Consistency |
|---|---|---|---|
| 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:
- 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.
- 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
| Component | Active-passive cost delta | Active-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:
- GA endpoint probe: connects to the GA anycast IPs from an external (non-AWS) network location to simulate a real-world client traversing GA routing. Performs full MCP SSE handshake, measures first-byte latency, issues
tools/list. This is what an MCP client sees. - Origin endpoint probe: bypasses GA by connecting directly to each regional NLB's DNS name (derived from the MCP server's metadata or our registry crawl). Measures each region's endpoint health independently of GA routing.
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.