Guide · AWS ECS Fargate

Fargate Task Definition for MCP Servers

A Fargate task definition is the immutable blueprint for your MCP server container — CPU allocation, memory limits, secrets injection, log routing, and signal handling are all set here. Most mistakes are invisible at startup but cause production failures: wrong CPU/memory combination — Fargate rejects invalid combinations at registration time, not at deploy time, with a cryptic InvalidParameterException; no init process (initProcessEnabled: true) — MCP servers that spawn child processes (shell commands, subprocess tool calls) accumulate zombie processes as PID 1 cannot reap them without init; default nofile ulimit of 1024 — an MCP server handling 100 concurrent sessions will exhaust file descriptors on the default limit because each WebSocket connection, database connection, and HTTP keepalive consumes one descriptor. These are the details that don't surface in tutorials but cause real production incidents.

TL;DR

Pick a valid CPU/memory combination from the Fargate table. Enable initProcessEnabled: true. Set nofile softLimit: 65536, hardLimit: 65536. Inject secrets from Secrets Manager via the secrets array (not environment variables). Use awslogs log driver with explicit log group name and 30-day retention. Set ephemeralStorage: 21 GiB minimum if your MCP tools write temp files.

Valid CPU and memory combinations

Fargate does not allow arbitrary CPU/memory values. The valid combinations are fixed — registering a task definition outside these values fails at registration time:

CPU (units) CPU (vCPU) Valid memory range (MiB) MCP server fit
256 0.25 vCPU 512–2048 (in 512 increments) Development only; single-digit concurrent tool calls
512 0.5 vCPU 1024–4096 (in 1024 increments) Low-traffic servers; under 20 concurrent tool calls
1024 1 vCPU 2048–8192 (in 1024 increments) Standard production MCP servers; up to 50 concurrent calls
2048 2 vCPU 4096–16384 (in 1024 increments) CPU-intensive tool calls (code execution, image processing)
4096 4 vCPU 8192–30720 (in 1024 increments) Memory-heavy MCP servers (in-memory caches, large datasets)
8192 8 vCPU 16384–61440 (in 4096 increments) High-throughput servers or multi-process MCP daemons
16384 16 vCPU 32768–122880 (in 8192 increments) Maximum Fargate size; batch-processing MCP tools

Memory is specified at the task level. Individual containers within the task can have memoryReservation (soft limit) and memory (hard limit) values that sum to at most the task-level memory. ECS OOM-kills a container that exceeds its hard memory limit; if no hard limit is set, it can consume all task memory and OOM-kill the task.

Complete task definition JSON

A production-ready task definition for a Node.js MCP server:

{
  "family": "mcp-server",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "1024",
  "memory": "2048",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "taskRoleArn": "arn:aws:iam::123456789012:role/mcpServerTaskRole",
  "ephemeralStorage": { "sizeInGiB": 21 },
  "containerDefinitions": [{
    "name": "mcp-server",
    "image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/mcp-server:v1.2.3",
    "essential": true,
    "initProcessEnabled": true,
    "cpu": 900,
    "memoryReservation": 1536,
    "memory": 1920,
    "portMappings": [{
      "name": "http",
      "containerPort": 3000,
      "protocol": "tcp",
      "appProtocol": "http"
    }],
    "environment": [
      { "name": "NODE_ENV", "value": "production" },
      { "name": "PORT", "value": "3000" },
      { "name": "LOG_LEVEL", "value": "warn" }
    ],
    "secrets": [
      {
        "name": "DATABASE_URL",
        "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/mcp-server/database-AbCdEf:connection_string::"
      },
      {
        "name": "API_KEY",
        "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/mcp-server/api-key"
      }
    ],
    "ulimits": [{
      "name": "nofile",
      "softLimit": 65536,
      "hardLimit": 65536
    }],
    "healthCheck": {
      "command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
      "interval": 10,
      "timeout": 5,
      "retries": 3,
      "startPeriod": 30
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/mcp-server",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "mcp-server"
      }
    }
  }]
}

Init process and zombie reaping

Linux PID 1 is responsible for reaping zombie processes — child processes that have finished execution but whose exit status has not been collected by their parent. In a container without an init process, your MCP server is PID 1 but Node.js (and most language runtimes) do not implement zombie reaping. MCP tools that spawn subprocesses — shell commands, Python scripts, data processing scripts — accumulate zombie entries in /proc over time. Under high load, this exhausts process table slots:

# Symptom: eventually fails with
# Error: EAGAIN: resource temporarily unavailable, fork

# Fix: enable the init process in the container definition
"initProcessEnabled": true

# This adds a minimal init process (tini) as PID 1 that:
# 1. Forwards signals (SIGTERM, SIGINT) to your application
# 2. Reaps zombie child processes automatically
# Adds ~100KB of overhead; negligible.

Signal forwarding is the second reason initProcessEnabled matters. Without it, the SIGTERM that ECS sends during scale-in or deployment goes to PID 1 (your Node.js process), but child processes spawned by child_process.spawn() run in their own process groups and do not receive the signal — they continue running until the container is force-killed after the 30-second grace period.

File descriptor limits for connection-heavy MCP servers

The default nofile ulimit in Fargate containers is 1024. An MCP server under moderate load can easily exhaust this:

Resource type File descriptors consumed Notes
WebSocket connection 1 per client MCP uses SSE or WebSocket — each session = 1 persistent connection
HTTP/1.1 keepalive to downstream API 1–5 per upstream service Connection pool per service
Database connection pool 10–50 per instance pg, mysql2, better-sqlite3 each consume descriptors
Node.js internals (stdio, IPC, libuv) ~20–30 Fixed overhead

With 100 concurrent WebSocket sessions + a database pool of 20 + 5 upstream connections + 30 Node internals = 155 descriptors — well under 1024. But under burst load (200 concurrent sessions) or with a connection leak, the default limit is reachable. Set both soft and hard limits to 65536 as a precaution:

"ulimits": [{
  "name": "nofile",
  "softLimit": 65536,
  "hardLimit": 65536
}]

Ephemeral storage

Fargate tasks get 20 GiB of ephemeral storage by default, shared between the container image layers and any writable storage. If your MCP tools write temporary files (file processing, code execution sandbox files, downloaded assets), you may need to increase this:

"ephemeralStorage": { "sizeInGiB": 50 }

Valid range is 21–200 GiB. The first 20 GiB are free; additional storage costs $0.10/GB/month (prorated to the task's runtime).

For MCP tools that write large temp files and then delete them, use /tmp inside the container — it writes to ephemeral storage. If the tool writes files that must persist across task restarts, use an EFS mount instead:

{
  "volumes": [{
    "name": "mcp-persistent-data",
    "efsVolumeConfiguration": {
      "fileSystemId": "fs-abc12345",
      "rootDirectory": "/mcp-server",
      "transitEncryption": "ENABLED",
      "authorizationConfig": { "accessPointId": "fsap-abc123", "iam": "ENABLED" }
    }
  }],
  "containerDefinitions": [{
    "mountPoints": [{
      "sourceVolume": "mcp-persistent-data",
      "containerPath": "/app/data",
      "readOnly": false
    }]
  }]
}

Secrets injection from Secrets Manager and SSM

Inject secrets via the secrets array — not via environment. The secrets array fetches values from Secrets Manager or SSM Parameter Store at task startup and injects them as environment variables. Values in environment are stored in plain text in the task definition and visible to anyone with ECS read access:

"secrets": [
  {
    "name": "DATABASE_URL",
    "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/mcp-server/database-AbCdEf:connection_string::"
  }
]

# The valueFrom format for Secrets Manager:
# arn:...:secret:SECRET_NAME:JSON_KEY::
# JSON_KEY extracts a single key from a JSON secret.
# Leave JSON_KEY empty to inject the entire secret string.

# For SSM Parameter Store:
{
  "name": "API_KEY",
  "valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/mcp-server/api-key"
}

The execution role (not the task role) needs permissions to fetch secrets:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "secretsmanager:GetSecretValue",
      "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/mcp-server/*"
    },
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameters",
      "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/mcp-server/*"
    },
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
    }
  ]
}

Common failures

Symptom Root cause Fix
InvalidParameterException: Memory value … is not supported when registering task definition CPU/memory combination outside the valid Fargate table Check the valid combinations table; e.g., 1024 CPU requires memory between 2048–8192 in 1024 increments
MCP server accumulates zombie processes over hours, eventually fails to fork new processes initProcessEnabled not set; Node.js as PID 1 cannot reap zombies from spawned child processes Add "initProcessEnabled": true to the container definition; redeploy
Task fails to start: CannotPullContainerError: Error response from daemon: toomanyrequests Execution role lacks permission to authenticate to ECR; pulling from public ECR without authentication hits rate limits Add ecr:GetAuthorizationToken, ecr:BatchGetImage, ecr:GetDownloadUrlForLayer to the execution role
Task fails to start: ResourceInitializationError: unable to pull secrets Execution role lacks secretsmanager:GetSecretValue or ssm:GetParameters; or the VPC has no route to the Secrets Manager endpoint Add required IAM permissions to execution role; add VPC endpoint for secretsmanager or ssm
MCP tool calls fail with EMFILE: too many open files Default nofile ulimit of 1024 exhausted by concurrent connections and file operations Set ulimits nofile softLimit: 65536, hardLimit: 65536 in the container definition
Container writes to disk succeed but files disappear on task restart Writing to ephemeral storage which is not persistent across task restarts For persistent storage, mount an EFS volume; ephemeral storage only survives the task's lifetime

Task definition mistakes cause silent MCP server failures

File descriptor exhaustion, zombie process buildup, and secrets injection failures all cause MCP tool calls to silently fail without crashing the container. AliveMCP probes every 60 seconds — you get alerted when the server becomes unresponsive, before the issue grows into a full outage.

Join the waitlist →