Guide · AWS Networking

MCP Server ALB — Application Load Balancer target groups, health checks, sticky sessions, WebSocket

The Application Load Balancer (ALB) is the entry point for HTTP-transport and SSE-transport MCP servers on ECS Fargate. Three configuration mistakes cause the most incidents: using instance target type instead of IP target type (ECS Fargate tasks use awsvpc networking — each task gets its own ENI and private IP; the target group must register tasks by their private IP, which means targetType: TargetType.IP; using the default INSTANCE target type causes the target group to show all targets as unhealthy because the instance has no port mapping for the task's ENI), using the MCP server's main route as the health check path (the MCP protocol handshake has initialization overhead and may return non-200 responses for protocol-negotiation requests; the health check must hit a dedicated /health or /ping endpoint that responds immediately with HTTP 200 — ALB marks a target unhealthy after 3 consecutive failures at a 30-second interval, a 90-second window that causes task replacement churn if the main route is slow), and missing sticky sessions for SSE transports (Server-Sent Events require the client to reconnect to the same task; without sticky sessions the ALB may route a reconnecting SSE client to a different task that has no state for that connection, causing the client to see a fresh event stream or a 404).

TL;DR

Set targetType: TargetType.IP for ECS awsvpc tasks. Create a dedicated GET /health endpoint that returns {"status":"ok"} in under 500ms. Enable duration-based sticky sessions (stickinessCookieDuration) on the target group if your MCP server uses SSE or WebSocket transport. Set deregistrationDelay: 30 (down from the default 300 seconds) to speed up rolling deploys without dropping in-flight connections.

Target group: IP type for ECS awsvpc

ECS Fargate tasks with awsvpc networking each receive a private ENI. The ALB registers these tasks by their ENI IP address, not by the host EC2 instance ID. If you create the target group with the default INSTANCE type, ECS cannot register the task, or registers it but the health check never passes.

// CDK: ALB + target group with IP target type for ECS Fargate
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";

const alb = new elbv2.ApplicationLoadBalancer(this, "McpAlb", {
  vpc,
  internetFacing: true,
  securityGroup: albSg,
});

const targetGroup = new elbv2.ApplicationTargetGroup(this, "McpTargetGroup", {
  vpc,
  targetType: elbv2.TargetType.IP,    // REQUIRED for ECS awsvpc networking
  port: 3000,
  protocol: elbv2.ApplicationProtocol.HTTP,
  healthCheck: {
    path: "/health",                  // dedicated health endpoint — not "/"
    interval: Duration.seconds(30),
    timeout: Duration.seconds(5),
    healthyThresholdCount: 2,
    unhealthyThresholdCount: 3,
    healthyHttpCodes: "200",
  },
  deregistrationDelay: Duration.seconds(30),  // default is 300s — too slow for rolling deploys
  stickinessCookieDuration: Duration.days(1), // enable sticky sessions for SSE/WebSocket
});

// HTTPS listener (TLS termination at ALB)
const httpsListener = alb.addListener("Https", {
  port: 443,
  protocol: elbv2.ApplicationProtocol.HTTPS,
  certificates: [elbv2.ListenerCertificate.fromArn(certArn)],
  defaultTargetGroups: [targetGroup],
});

// HTTP → HTTPS redirect
alb.addListener("Http", {
  port: 80,
  defaultAction: elbv2.ListenerAction.redirect({
    protocol: "HTTPS",
    port: "443",
    permanent: true,
  }),
});

ECS registers the Fargate task IP into the target group automatically when using the ApplicationLoadBalancedFargateService construct, or when the FargateService is attached to the target group via service.attachToApplicationTargetGroup(targetGroup).

Health check endpoint

The health check path must respond immediately without triggering MCP protocol initialization. A separate express route (or equivalent) that returns 200 in under 500ms is the correct pattern.

// Node.js: lightweight health endpoint that does not initialize MCP session
import express from "express";
import { createServer } from "@modelcontextprotocol/sdk/server/index.js";

const app = express();

// Health endpoint — must respond before any MCP setup
app.get("/health", (_req, res) => {
  res.json({ status: "ok", timestamp: Date.now() });
});

// MCP SSE endpoint (or Streamable HTTP)
app.get("/mcp", async (req, res) => {
  // ... MCP transport setup
});

// ALB checks /health every 30 seconds
// The health endpoint must return 200 within the 5-second timeout configured above
// If /health returns 200 for 2 consecutive checks (healthyThresholdCount: 2),
// the target is marked healthy; 3 failures marks it unhealthy
app.listen(3000);

Do not use / as the health check path if it triggers middleware chains, authentication checks, or session initialization. The ALB sends a bare GET /health HTTP/1.1 with no auth headers — any 401/403 response marks the target unhealthy.

Sticky sessions for SSE and WebSocket transports

MCP servers using Server-Sent Events (SSE) or WebSocket transport maintain per-connection state in memory. If the ALB routes a client reconnection to a different task, the new task has no state for that session. Sticky sessions pin a client to the same target for the duration of the session.

TransportSticky sessions needed?Why
HTTP (stateless JSON-RPC over HTTP)NoEach request is independent; any task can handle any request
Streamable HTTPDependsIf server uses in-memory session state per Mcp-Session-Id, yes; if state is in Redis/DB, no
SSE (legacy transport)YesSSE is a persistent connection; client reconnects must reach the same task
WebSocketYes (initial only)ALB routes the upgrade to one task; the connection stays on that task until closed
// CDK: sticky sessions via duration-based cookie
const targetGroup = new elbv2.ApplicationTargetGroup(this, "McpTargetGroup", {
  // ...
  stickinessCookieDuration: Duration.hours(24),
  // ALB sets AWSALB cookie on first response
  // Subsequent requests from the same client include the cookie
  // ALB routes cookie-bearing requests to the same registered target
});

// Alternative: application-controlled cookie (AWSALBAPP)
// Use this if you want to control the cookie name or set it from your app
const targetGroup2 = new elbv2.ApplicationTargetGroup(this, "McpTargetGroupApp", {
  stickinessCookieDuration: Duration.hours(24),
  stickinessCookieName: "MCPSESSION",   // application stickiness — requires app to set this cookie
});

Sticky sessions and rolling deploys: when a task is deregistered (during a rolling deploy), the ALB immediately stops routing new connections to it. Existing sticky sessions on that task will fail when the client next requests — the deregistrationDelay window allows in-flight requests to complete, but long-lived SSE connections will be interrupted. Design your MCP client to reconnect automatically when an SSE connection drops.

Common failure modes

SymptomCauseFix
All ECS targets show "unhealthy" in target groupTarget group uses INSTANCE target type instead of IP for ECS awsvpc tasksRecreate target group with targetType: IP; ECS deregisters and re-registers automatically
Health check passes locally but fails in ALBHealth check path triggers auth middleware that returns 401 to ALB's source IPExempt the health check path from authentication middleware
Rolling deploy takes 5+ minutes per taskderegistrationDelay is 300s (default) and tasks wait for the full drain windowReduce deregistrationDelay to 30s for stateless services; keep 60–120s for SSE services to drain existing connections
SSE clients see blank event stream on reconnectSticky sessions not enabled; reconnect routed to different task with no session stateEnable sticky sessions on the target group or externalize session state to Redis
WebSocket upgrade returns 400 from ALBALB listener protocol is set to HTTP/1.0 or the target group protocol mismatchALB supports WebSocket on HTTP/1.1 listeners automatically; ensure target group protocol is HTTP (not HTTPS unless the task itself terminates TLS)
ALB returns 504 after 60 seconds on long-running tool callsALB idle timeout is 60s; long MCP tool executions exceed itIncrease ALB idleTimeout to 300s or use SSE streaming to send keepalive events every 30s to reset the idle timer