Guide · AWS ECS Fargate

ECS Container Insights for MCP Servers

ECS Container Insights collects CPU, memory, network, and storage utilization metrics at both the task and container level without any code changes — but it costs $0.50 per 1,000 metrics per month and is disabled by default. For MCP server operators the trade-off is clear: without Container Insights, the only CPU/memory metrics you see are at the service level (the average across all tasks), which masks individual task saturation. A single overloaded task handling a runaway tool call will not appear in the aggregate average until it's already causing failures. Three common mistakes: enabling Container Insights after the cluster exists but forgetting to enable it on each individual service — cluster-level and service-level settings are separate; not configuring log retention — the default is infinite retention, and MCP server verbose logs accumulate quickly at $0.50/GB/month; relying solely on Container Insights CPU metrics without custom application-level metrics — CPU 50% tells you a task is busy but not whether it is busy processing tool calls or stuck in a retry loop.

TL;DR

Enable Container Insights on the cluster with --configuration containerInsights=enabled. Set a 30-day retention policy on all /ecs/ log groups. Emit application-level metrics (tool call count, latency, error rate) via Embedded Metric Format (EMF) to stdout — they appear in CloudWatch Metrics automatically. Alarm on MemoryUtilization > 85% per task (OOM kills for MCP servers are silent — the task stops, the tool call is dropped, and the ALB retries to a different task).

Enabling Container Insights

Container Insights is a cluster-level setting. Enable it at creation or update an existing cluster:

# Enable at cluster creation
aws ecs create-cluster \
  --cluster-name my-cluster \
  --configuration '{"executeCommandConfiguration": {}, "containerInsights": "enabled"}'

# Enable on an existing cluster
aws ecs update-cluster-settings \
  --cluster my-cluster \
  --settings name=containerInsights,value=enabled

In CDK:

const cluster = new ecs.Cluster(this, 'Cluster', {
  vpc,
  containerInsights: true,
});

Container Insights has two tiers:

Tier What you get Cost When to use
Standard (enabled) Task-level CPU, memory, network, storage metrics; container-level CPU and memory breakdowns; pre-built CloudWatch dashboards $0.50/1,000 metrics/month; typically $2–5/month per active service Production MCP servers — the task-level breakdown is worth the cost for debugging saturation
Enhanced (enabled_with_insights) Everything in Standard plus GPU metrics, deeper network I/O per container, application signals integration ~2× Standard cost Only if you use GPU-based MCP tools or need CloudWatch Application Signals integration

Metrics available per task

Container Insights emits metrics under the ECS/ContainerInsights namespace with dimensions ClusterName, ServiceName, TaskDefinitionFamily:

Metric Alarm threshold for MCP servers Why
CpuUtilized (vCPU) > 80% of CpuReserved Sustained CPU saturation causes tool call latency to spike; scale-out threshold
MemoryUtilized (MiB) > 85% of MemoryReserved Fargate OOM-kills the task without a log entry when memory is exceeded; the task simply stops
NetworkRxBytes Deviation > 3σ from baseline Sudden spike = large tool call payload or data exfiltration pattern
StorageReadBytes Sustained high rate Ephemeral storage reads exceeding capacity cause task-level I/O throttling
RunningTaskCount < DesiredCount Tasks not running = MCP server capacity below target; alarm + alert
PendingTaskCount > 0 for > 5 minutes Tasks stuck in PENDING = Fargate capacity issue or task-level error (image pull failure, IAM)

Container-level metrics (with ContainerName dimension) give you a breakdown per container inside a task definition. Useful for multi-container tasks where an app container and a sidecar (e.g., FireLens log router) share the same task CPU.

Custom application metrics via EMF

Container Insights collects infrastructure metrics automatically. For application-level metrics (MCP tool call count, per-tool latency, error rate by tool name), use Embedded Metric Format (EMF) — write structured JSON to stdout and CloudWatch Logs agent converts it to CloudWatch Metrics automatically, with no API calls or SDK dependencies:

function emitToolMetric(toolName: string, durationMs: number, success: boolean): void {
  const emfPayload = {
    _aws: {
      Timestamp: Date.now(),
      CloudWatchMetrics: [{
        Namespace: "MCPServer/Tools",
        Dimensions: [["ToolName"], ["ToolName", "ServiceName"]],
        Metrics: [
          { Name: "ToolCallDuration", Unit: "Milliseconds" },
          { Name: "ToolCallCount", Unit: "Count" },
          { Name: "ToolCallErrors", Unit: "Count" },
        ],
      }],
    },
    // Dimension values
    ToolName: toolName,
    ServiceName: process.env.ECS_SERVICE_NAME ?? "mcp-server",
    // Metric values
    ToolCallDuration: durationMs,
    ToolCallCount: 1,
    ToolCallErrors: success ? 0 : 1,
  };
  // Write to stdout — CloudWatch Logs agent picks it up
  process.stdout.write(JSON.stringify(emfPayload) + "\n");
}

// Usage in your MCP tool handler
async function handleToolCall(name: string, args: unknown): Promise<unknown> {
  const start = Date.now();
  let success = false;
  try {
    const result = await executeToolLogic(name, args);
    success = true;
    return result;
  } finally {
    emitToolMetric(name, Date.now() - start, success);
  }
}

This produces metrics in a custom MCPServer/Tools namespace, dimensioned by ToolName and ServiceName. You can alarm on ToolCallErrors / ToolCallCount > 0.05 (5% error rate per tool) without any third-party APM.

Log groups and retention

Fargate tasks write to CloudWatch Logs via the awslogs log driver. Each service gets its own log group. Without explicit retention, logs accumulate indefinitely at $0.50/GB/month ingestion + $0.03/GB/month storage:

# Set 30-day retention on an existing log group
aws logs put-retention-policy \
  --log-group-name /ecs/mcp-server \
  --retention-in-days 30

# Automate for all /ecs/ log groups
aws logs describe-log-groups \
  --log-group-name-prefix /ecs/ \
  --query 'logGroups[?!retentionInDays].logGroupName' \
  --output text | tr '\t' '\n' | while read lg; do
    aws logs put-retention-policy --log-group-name "$lg" --retention-in-days 30
    echo "Set 30d retention on $lg"
  done

In the task definition, configure the log driver explicitly with a group name and stream prefix:

{
  "logConfiguration": {
    "logDriver": "awslogs",
    "options": {
      "awslogs-group": "/ecs/mcp-server",
      "awslogs-region": "us-east-1",
      "awslogs-stream-prefix": "mcp-server",
      "awslogs-create-group": "true"
    }
  }
}

Use awslogs-create-group: "true" only in development — in production, create log groups explicitly with a retention policy before the service starts, to prevent the Fargate task from creating a group with infinite retention.

Correlating Container Insights with application metrics

Container Insights task ARNs and application-level EMF metrics share no common dimension by default. Add the task ARN as a dimension to EMF metrics to correlate a spike in ToolCallErrors with a specific task's CPU saturation:

const TASK_METADATA_URI = process.env.ECS_CONTAINER_METADATA_URI_V4;
let taskArn: string | undefined;

async function getTaskArn(): Promise<string> {
  if (!taskArn) {
    const metadata = await fetch(`${TASK_METADATA_URI}/task`).then(r => r.json());
    taskArn = metadata.TaskARN;
  }
  return taskArn!;
}

// Include TaskARN in EMF dimensions
async function emitEnrichedToolMetric(toolName: string, durationMs: number, success: boolean): Promise<void> {
  const arn = await getTaskArn();
  const emfPayload = {
    _aws: {
      Timestamp: Date.now(),
      CloudWatchMetrics: [{
        Namespace: "MCPServer/Tools",
        Dimensions: [["ToolName"], ["ToolName", "TaskArn"]],
        Metrics: [
          { Name: "ToolCallDuration", Unit: "Milliseconds" },
          { Name: "ToolCallErrors", Unit: "Count" },
        ],
      }],
    },
    ToolName: toolName,
    TaskArn: arn,
    ToolCallDuration: durationMs,
    ToolCallErrors: success ? 0 : 1,
  };
  process.stdout.write(JSON.stringify(emfPayload) + "\n");
}

High cardinality dimensions (TaskArn changes with every deployment) produce many CloudWatch metric streams — $0.30/stream/month × unique TaskArns. Use the task ARN dimension only for debug dashboards, not production alarms.

Common failures

Symptom Root cause Fix
Container Insights metrics not appearing in CloudWatch after enabling Cluster setting changed but existing tasks were not replaced; Container Insights requires tasks to be redeployed Force a new deployment: aws ecs update-service --cluster my-cluster --service mcp-server-svc --force-new-deployment
Task OOM-killed with no log entry Fargate kills the task before the memory exceeded log line is written; the task just stops Alarm on MemoryUtilized > 85% of MemoryReserved; catch the approach before the kill, not after
EMF metrics appear in logs but not in CloudWatch Metrics EMF JSON is not on a single line (multi-line EMF is not parsed); or the _aws block is malformed Ensure JSON.stringify(emfPayload) followed by a single \n; validate the _aws.CloudWatchMetrics structure
Log group ingestion costs unexpectedly high MCP server emitting verbose debug logs at INFO level in production; no retention policy set Set LOG_LEVEL=warn in production task definition; add 30-day retention policy on all /ecs/ log groups
PendingTaskCount stays > 0 indefinitely Task definition references an ECR image that doesn't exist; execution role lacks ecr:GetDownloadUrlForLayer Check ECS service events (aws ecs describe-services); verify ECR image tag exists and execution role has ECR pull permissions

External monitoring fills the gaps Container Insights misses

Container Insights shows infrastructure health — CPU, memory, task count. It does not tell you if your MCP server is returning valid responses to callers. AliveMCP makes an actual protocol-level probe every 60 seconds and alerts on response failures, even when all containers are running and all metrics look normal.

Join the waitlist →