Guide · AWS ECS Fargate

ECS Service Auto Scaling for MCP Servers

ECS service auto scaling adjusts the number of running Fargate tasks to match traffic load, but MCP servers have two properties that make naive scaling dangerous. First, MCP tool calls are stateful within a session — a task that receives a scale-in termination signal (SIGTERM) mid-tool-call drops that call with no retry at the client. Second, MCP servers often have bursty load patterns — an LLM agent may dispatch 20 parallel tool calls within 200ms, then idle for 30 seconds while the LLM processes results. The default ALBRequestCountPerTarget target tracking policy scales on active request count, but it reacts too slowly for burst traffic and too aggressively on scale-in without protection configured. Three common mistakes: scale-in protection not enabled on tasks with active calls — Fargate terminates mid-call tasks; scale-in cooldown too short — tasks spin down during the LLM processing pause between tool call batches; no maximum capacity set — a runaway agent dispatching thousands of calls exhausts your Fargate vCPU quota before you notice.

TL;DR

Register your ECS service with Application Auto Scaling. Use target tracking on ALBRequestCountPerTarget with target 50 and scale-out cooldown 60s / scale-in cooldown 300s. Enable scale-in protection in your container code while a tool call is executing. Set a hard maximum capacity to prevent runaway scaling. For predictable load patterns (scheduled agent workflows), add a scheduled scaling action to pre-warm capacity.

Registering a scalable target

Before attaching any scaling policy, register the ECS service as a scalable target with Application Auto Scaling. This is the resource declaration that all policies attach to:

aws application-autoscaling register-scalable-target \
  --service-namespace ecs \
  --resource-id service/my-cluster/mcp-server-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --min-capacity 2 \
  --max-capacity 20

Always set --min-capacity to at least 2 for production MCP servers. A minimum of 1 means any transient task failure (OOM kill, Fargate node reclaim) takes the service completely down while a replacement task provisions (typically 20–60 seconds).

In CDK, this looks like:

import * as autoscaling from 'aws-cdk-lib/aws-applicationautoscaling';

const scalableTarget = service.autoScaleTaskCount({
  minCapacity: 2,
  maxCapacity: 20,
});

Target tracking: ALBRequestCountPerTarget

Target tracking is the recommended first scaling policy. It automatically creates CloudWatch alarms and adjusts task count to keep the chosen metric at the target value. For an HTTP-based MCP server behind an ALB:

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-cluster/mcp-server-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name mcp-server-target-tracking \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration '{
    "TargetValue": 50.0,
    "PredefinedMetricSpecification": {
      "PredefinedMetricType": "ALBRequestCountPerTarget",
      "ResourceLabel": "app/my-alb/abc123/targetgroup/mcp-server-tg/def456"
    },
    "ScaleOutCooldown": 60,
    "ScaleInCooldown": 300,
    "DisableScaleIn": false
  }'

The ResourceLabel is the suffix of the target group ARN combined with the ALB ARN. Retrieve it from the ALB load balancer ARN and target group ARN:

# Format: app///targetgroup//
# Extract from ALB ARN: arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/abc123
# → app/my-alb/abc123
# Extract from TG ARN: arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/mcp-server-tg/def456
# → targetgroup/mcp-server-tg/def456
RESOURCE_LABEL="app/my-alb/abc123/targetgroup/mcp-server-tg/def456"
Parameter Recommended value Rationale for MCP servers
TargetValue 50 MCP tool calls can be CPU-intensive; 50 concurrent requests per task leaves headroom for processing overhead before the next task starts accepting traffic
ScaleOutCooldown 60s New Fargate tasks take 20–40s to provision and pass health checks; 60s ensures the new capacity is registered before the next scale-out evaluation
ScaleInCooldown 300s MCP workloads have bursty patterns — the LLM may idle for 30–60s between tool call batches; a 300s cooldown prevents scale-in during idle windows that precede another burst
DisableScaleIn false (default) Keep scale-in enabled; use per-task scale-in protection (below) to protect active calls, not a global disable which prevents the service from ever shrinking

Scale-in protection for active tool calls

When Application Auto Scaling decides to scale in, ECS selects tasks to terminate and sends them SIGTERM. Without scale-in protection, a task may be terminated while it is mid-execution on a tool call, dropping the call with no retry at the client. ECS provides per-task scale-in protection — set it from inside the container while a call is in flight:

import { ECSClient, UpdateTaskProtectionCommand } from "@aws-sdk/client-ecs";

const ecsClient = new ECSClient({ region: process.env.AWS_REGION });
const CLUSTER = process.env.ECS_CLUSTER_NAME!;
const TASK_ARN = process.env.ECS_TASK_ARN!; // injected by ECS via task metadata

async function protectTaskDuringToolCall<T>(fn: () => Promise<T>): Promise<T> {
  // Enable scale-in protection before executing the tool call
  await ecsClient.send(new UpdateTaskProtectionCommand({
    cluster: CLUSTER,
    tasks: [TASK_ARN],
    protectionEnabled: true,
    expiresInMinutes: 60, // safety ceiling; adjust to your max tool call duration
  }));

  try {
    return await fn();
  } finally {
    // Disable protection after the call completes or fails
    await ecsClient.send(new UpdateTaskProtectionCommand({
      cluster: CLUSTER,
      tasks: [TASK_ARN],
      protectionEnabled: false,
    }));
  }
}

The task metadata endpoint injects ECS_TASK_ARN automatically in Fargate tasks. Retrieve it from the task metadata endpoint if not already available as an environment variable:

const METADATA_URI = process.env.ECS_CONTAINER_METADATA_URI_V4;
const taskMetadata = await fetch(`${METADATA_URI}/task`).then(r => r.json());
const TASK_ARN = taskMetadata.TaskARN;

The IAM policy required on the task role:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "ecs:UpdateTaskProtection",
    "Resource": "arn:aws:ecs:*:*:task/my-cluster/*"
  }]
}

If the ECS service has a mix of idle and active tasks, ECS will preferentially terminate unprotected (idle) tasks during scale-in, leaving protected tasks running until their protection expires or is cleared.

Step scaling for burst traffic

Target tracking is reactive — it scales out after the metric breaches the target. For MCP servers with sudden bursts (an agent dispatching 50 parallel calls simultaneously), step scaling on a CloudWatch alarm fires faster because the alarm evaluates every 60 seconds rather than waiting for the tracking algorithm to converge:

aws application-autoscaling put-scaling-policy \
  --service-namespace ecs \
  --resource-id service/my-cluster/mcp-server-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --policy-name mcp-server-step-scale-out \
  --policy-type StepScaling \
  --step-scaling-policy-configuration '{
    "AdjustmentType": "ChangeInCapacity",
    "StepAdjustments": [
      { "MetricIntervalLowerBound": 0, "MetricIntervalUpperBound": 50, "ScalingAdjustment": 2 },
      { "MetricIntervalLowerBound": 50, "MetricIntervalUpperBound": 100, "ScalingAdjustment": 4 },
      { "MetricIntervalLowerBound": 100, "ScalingAdjustment": 8 }
    ],
    "Cooldown": 60,
    "MetricAggregationType": "Average"
  }'

Attach a CloudWatch alarm to trigger the step policy when CPU utilization exceeds 70%:

aws cloudwatch put-metric-alarm \
  --alarm-name mcp-server-cpu-high \
  --metric-name CPUUtilization \
  --namespace AWS/ECS \
  --dimensions Name=ClusterName,Value=my-cluster Name=ServiceName,Value=mcp-server-svc \
  --statistic Average \
  --period 60 \
  --threshold 70 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 2 \
  --alarm-actions arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:abc:resource/ecs/service/my-cluster/mcp-server-svc:policyName/mcp-server-step-scale-out

Scheduled scaling for predictable workloads

If your MCP server serves a scheduled agent workflow (daily batch job, nightly reports), pre-warm capacity with a scheduled scaling action to avoid scale-out latency at job start:

aws application-autoscaling put-scheduled-action \
  --service-namespace ecs \
  --resource-id service/my-cluster/mcp-server-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --scheduled-action-name mcp-server-morning-scaleout \
  --schedule "cron(0 8 * * ? *)" \
  --scalable-target-action MinCapacity=10,MaxCapacity=20

aws application-autoscaling put-scheduled-action \
  --service-namespace ecs \
  --resource-id service/my-cluster/mcp-server-svc \
  --scalable-dimension ecs:service:DesiredCount \
  --scheduled-action-name mcp-server-night-scalein \
  --schedule "cron(0 20 * * ? *)" \
  --scalable-target-action MinCapacity=2,MaxCapacity=20

The schedule uses UTC cron expressions. Pre-warming 10 minutes before the job starts is sufficient — Fargate task provisioning takes 20–60 seconds, so new tasks are healthy well before the first tool call arrives.

Common failures

Symptom Root cause Fix
MCP tool calls drop with no error during traffic surge Scale-in fired during LLM idle window between tool call batches; new tasks haven't started yet when the next burst arrives Increase ScaleInCooldown to 300s; add scale-in protection during active calls
Tasks terminate mid-call with SIGTERM Scale-in selected an active task; scale-in protection was not enabled Call UpdateTaskProtection at the start of each tool call handler; ensure task role has ecs:UpdateTaskProtection
Service fails to scale out: InvalidParameterException: Desired count exceeds maximum capacity MaxCapacity set too low; burst traffic needs more tasks than the ceiling allows Raise MaxCapacity; check Fargate vCPU quotas in your account (Service Quotas console)
Target tracking policy cannot find metric: ResourceLabel not found ResourceLabel in the policy config doesn't match the actual ALB + target group ARN suffixes Re-derive the label from the ARNs: app/<alb-name>/<alb-id>/targetgroup/<tg-name>/<tg-id>
Scale-out doesn't trigger despite high CPU CloudWatch alarm uses EvaluationPeriods: 3 (default) — takes 3 minutes before firing; burst is over by then Set EvaluationPeriods: 1 or 2 on the scale-out alarm; combine with target tracking for continuous adjustment

Know when scaling events precede MCP server outages

Scale-in terminations, misconfigured cooldowns, and capacity shortfalls all cause MCP server downtime. AliveMCP probes every 60 seconds — you get an alert the moment a scaling event takes the service below healthy capacity, not 10 minutes later from a user complaint.

Join the waitlist →