This website uses cookies

Read our Privacy policy and Terms of use for more information.

What it actually is

Fargate is a compute engine for containers. You hand ECS (or EKS) a container image and a size, and AWS finds a place to run it. There is no instance in your account to patch, drain, scale or SSH into. The unit of everything, from isolation to billing, is the task.

The part the marketing skips: Fargate is not a service you call. It is a launch type, a way of answering "where does this task run" that ECS accepts. You still write task definitions, create clusters, run services, attach load balancers. Fargate only removes the question of which machine.

The model

Each Fargate task runs in its own isolated environment. The docs state it plainly: tasks do not share network interfaces, ephemeral storage, CPU or memory with other tasks. Containers inside the same task do share a network namespace, so they talk over localhost, and they share the task's ephemeral storage.

Three consequences follow from that design.

First, networking is always awsvpc. Every task gets its own elastic network interface in your subnet, with a private IP and your security groups attached. That is great for security groups per task. It also means your subnet's IP space is your task capacity, and image pulls need a route: a public IP on the task, a NAT gateway, or VPC endpoints for ECR.

Second, you size the task, not the machine. CPU and memory come in fixed combinations. Linux tasks go from 0.25 vCPU with 512 MiB up through 16 vCPU with up to 120 GB, and as of June 2026 up to 32 vCPU with 60, 120 or 244 GB of memory (announced June 5, 2026, available on both Fargate and Fargate Spot, Linux, x86 and ARM). The 8, 16 and 32 vCPU sizes need Linux platform version 1.4.0 or later.

Third, there is no host. Privileged containers are unsupported, so Docker-in-Docker is out. Neither you nor AWS operators can connect to the host. The only added Linux capability you get is SYS_PTRACE, meant for observability and security agents. For debugging you use ECS Exec. If you need privileged mode or a GPU, look at the EC2 launch type or Amazon ECS Managed Instances, covered below.

Storage is 20 GiB of ephemeral space by default on platform 1.4.0, configurable up to 200 GiB with the ephemeralStorage parameter, encrypted at rest with AES-256 by default. The container image itself lives in that space, so the space available to your app is the total minus the image size. Platform version 1.3.0 reached its deprecation date on June 15, 2026. If a service still pins it, it is no longer supported. Move to 1.4.0 or just leave the platform version as LATEST.

What AWS operates: the hosts, the kernel, the container runtime, the patching, the placement. What you operate: the task definition, the image, the network, the IAM roles, and the scaling policy that decides how many tasks exist.

When to use it, when not to

Need

Fargate

EC2 launch type

ECS Managed Instances

Lambda

Unit you pay for

Task vCPU-seconds and GB-seconds

The whole instance

The whole instance, no extra management fee

Request plus GB-seconds

Bin-packing many small tasks

No, one task per isolated environment

Yes, you pack them

Yes, AWS packs them

n/a

Privileged mode, GPUs, custom AMI

No

Yes

Privileged capabilities yes, custom AMI no

No

Long-running steady load at high utilization

Most expensive per vCPU

Cheapest if you keep it full

Cheapest with less operations

Rarely

Spiky, bursty, scale to zero

Good, pay per second after a 1-minute minimum

Poor, you pay for idle instances

Poor

Best under 15 minutes

Operational burden

Lowest

Highest

Low

Lowest

The honest rule: Fargate wins when your utilization would be low or lumpy on instances, when your team is small, or when per-task isolation is a requirement. EC2 or Managed Instances wins when you can keep a fleet busy and want the per-vCPU price down. Managed Instances is AWS-managed EC2 (Bottlerocket only, instances recycled after 14 days, billed at plain EC2 On-Demand rates with no management fee per the docs) and accepts existing Fargate task definitions on platform 1.4.0, so you can move between them without rewriting.

What it costs

Fargate bills on vCPU-seconds and GB-seconds, plus extra ephemeral storage beyond the free 20 GiB. In us-east-1, Linux x86 is $0.000011244 per vCPU-second and $0.000001235 per GB-second. Linux ARM is $0.0000089944 per vCPU-second and $0.0000009889 per GB-second. Extra storage is $0.0000000308 per GB-second. Linux tasks bill per second with a one-minute minimum (Windows has a five-minute minimum).

Those numbers are unreadable per second, so here they are per hour for common sizes, computed from the rates above.

Task size

x86 per hour

x86 per 730h month

ARM per hour

ARM per 730h month

0.25 vCPU, 0.5 GB

$0.01234

$9.01

$0.00987

$7.21

1 vCPU, 2 GB

$0.04937

$36.04

$0.03950

$28.83

2 vCPU, 4 GB

$0.09874

$72.08

$0.07900

$57.67

4 vCPU, 8 GB

$0.19748

$144.16

$0.15800

$115.34

ARM is 20 percent cheaper at every size, because both rates are 20 percent lower. If your image builds for arm64, that is the cheapest decision in this article. Extra storage is small: 100 GiB total (80 GiB billable) costs about $0.0089 per hour, roughly $6.48 per month always-on.

Fargate Spot runs interruption-tolerant tasks on spare capacity at up to 70 percent off, with a two-minute warning. Compute Savings Plans offer up to 50 percent off in exchange for a one or three year commitment, and apply to Fargate, including the new 32 vCPU sizes.

The surprise is not on the Fargate line. It is the network. A task in a private subnet pulling images and calling AWS APIs needs a NAT gateway, at $0.045 per hour plus $0.045 per GB processed in the region I checked, which can exceed the compute bill of a small service. A task in a public subnet needs a public IPv4 address, charged at $0.005 per hour per address. Five always-on tasks with public IPs add about $18 a month on top of compute. VPC endpoints for ECR, S3 and CloudWatch Logs are how you get both numbers down in production.

There is no Fargate free tier worth planning around. Treat every task-hour as billable.

The limits that bite

New accounts start with a Fargate On-Demand vCPU quota of 6 vCPUs per region, and a separate 6 vCPUs for Fargate Spot, both adjustable in Service Quotas. Six vCPUs is six 1-vCPU tasks. A load test that scales to 20 tasks gets throttled and the service events say so. Raise the quota before you need it.

Launch rate is the second limit. In the major regions the burst is 100 tasks and the sustained rate is 20 tasks per second for On-Demand, with 25 and 5 in other regions. A service launches at most 500 tasks per minute in major regions (125 in regions introduced after May 20, 2025), not adjustable. A service holds at most 5,000 tasks. These are why "scale from 0 to 2,000 tasks in a minute" is a plan that needs arithmetic.

Third, cold starts are real. Fargate pulls your image fresh onto a fresh environment for each task. A 1.5 GB image makes every scale-out event slow, and the image counts against ephemeral storage. Small images are a performance feature here.

Fourth, per-task isolation means you cannot share a warm cache or a sidecar host across tasks. Anything shared has to be a network service or EFS.

Build it

The deliverable: one Python script that registers the same benchmark container as three runs (x86 On-Demand, ARM On-Demand, x86 Spot), measures work done per run, and prints the cost per run and cost per million operations. You will see the ARM and Spot economics on your own bill instead of mine.

Estimated cost to follow along: three runs of one 1-vCPU, 2 GB task, a minute or two each. At the rates above that is a few cents total, plus cents of CloudWatch Logs. The tasks use the default VPC with public IPs for about $0.005 per hour each, rounded to nothing.

Prerequisites

You need an AWS account with a default VPC in us-east-1, AWS CLI v2 configured, and Python 3.10+ with boto3 (pip install boto3). The task image is public.ecr.aws/docker/library/python:3.12-slim, a public multi-architecture image that runs on both x86 and arm64.

IAM

The identity that runs the script needs permission to create the cluster, task definitions, a log group and an execution role. For a sandbox account, this inline policy covers it. Tighten the resources for anything beyond a sandbox.

{
  "Version": "2012-10-17",
  "Statement": [
    {"Effect": "Allow", "Action": [
      "ecs:CreateCluster", "ecs:DeleteCluster", "ecs:DescribeClusters",
      "ecs:RegisterTaskDefinition", "ecs:DeregisterTaskDefinition",
      "ecs:RunTask", "ecs:DescribeTasks", "ecs:ListTasks", "ecs:StopTask",
      "ecs:PutClusterCapacityProviders",
      "logs:CreateLogGroup", "logs:DeleteLogGroup", "logs:GetLogEvents",
      "ec2:DescribeVpcs", "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups",
      "pricing:GetProducts",
      "iam:CreateRole", "iam:DeleteRole", "iam:AttachRolePolicy",
      "iam:DetachRolePolicy", "iam:GetRole"
    ], "Resource": "*"},
    {"Effect": "Allow", "Action": "iam:PassRole",
     "Resource": "arn:aws:iam::*:role/sl107-fargate-exec",
     "Condition": {"StringEquals": {"iam:PassedToService": "ecs-tasks.amazonaws.com"}}}
  ]
}

The task execution role is what lets Fargate itself write logs on the task's behalf. The script creates it with the managed AmazonECSTaskExecutionRolePolicy.

The script

Save as fargate_bench.py. It has three subcommands: setup, run, cleanup. The benchmark is deliberately boring: a single-threaded SHA-256 loop for 60 seconds that prints how many hashes it completed. One thread on one vCPU means ops per second is a fair proxy for what you bought.

import json, sys, time, boto3

REGION = "us-east-1"
CLUSTER = "sl107-bench"
ROLE = "sl107-fargate-exec"
LOG_GROUP = "/sl107/bench"
IMAGE = "public.ecr.aws/docker/library/python:3.12-slim"

# us-east-1 Linux Fargate rates, USD per second (verified 2026-10-01)
RATES = {
    "X86_64": {"vcpu": 0.000011244, "gb": 0.000001235},
    "ARM64":  {"vcpu": 0.0000089944, "gb": 0.0000009889},
}
CPU, MEM_MIB = 1024, 2048

CODE = (
    "import time,hashlib\n"
    "t=time.time();n=0\n"
    "while time.time()-t<60:\n"
    "    hashlib.sha256(b'x'*1024).digest();n+=1\n"
    "print('RESULT',n)\n"
)

ecs = boto3.client("ecs", region_name=REGION)
ec2 = boto3.client("ec2", region_name=REGION)
iam = boto3.client("iam")
logs = boto3.client("logs", region_name=REGION)


def setup():
    trust = {"Version": "2012-10-17", "Statement": [{
        "Effect": "Allow",
        "Principal": {"Service": "ecs-tasks.amazonaws.com"},
        "Action": "sts:AssumeRole"}]}
    try:
        iam.create_role(RoleName=ROLE, AssumeRolePolicyDocument=json.dumps(trust))
    except iam.exceptions.EntityAlreadyExistsException:
        pass
    iam.attach_role_policy(
        RoleName=ROLE,
        PolicyArn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy")
    try:
        logs.create_log_group(logGroupName=LOG_GROUP)
    except logs.exceptions.ResourceAlreadyExistsException:
        pass
    ecs.create_cluster(clusterName=CLUSTER,
                       capacityProviders=["FARGATE", "FARGATE_SPOT"])
    role_arn = iam.get_role(RoleName=ROLE)["Role"]["Arn"]
    for arch in ("X86_64", "ARM64"):
        ecs.register_task_definition(
            family=f"sl107-bench-{arch.lower()}",
            networkMode="awsvpc",
            requiresCompatibilities=["FARGATE"],
            cpu=str(CPU), memory=str(MEM_MIB),
            runtimePlatform={"cpuArchitecture": arch,
                             "operatingSystemFamily": "LINUX"},
            executionRoleArn=role_arn,
            containerDefinitions=[{
                "name": "bench", "image": IMAGE, "essential": True,
                "command": ["python", "-c", CODE],
                "stopTimeout": 30,
                "logConfiguration": {"logDriver": "awslogs", "options": {
                    "awslogs-group": LOG_GROUP,
                    "awslogs-region": REGION,
                    "awslogs-stream-prefix": "bench"}}}])
    print("setup done (IAM changes can take a few seconds to propagate)")


def network():
    vpc = ec2.describe_vpcs(Filters=[{"Name": "isDefault", "Values": ["true"]}])["Vpcs"][0]["VpcId"]
    subnets = [s["SubnetId"] for s in ec2.describe_subnets(
        Filters=[{"Name": "vpc-id", "Values": [vpc]}])["Subnets"]]
    sg = ec2.describe_security_groups(Filters=[
        {"Name": "vpc-id", "Values": [vpc]},
        {"Name": "group-name", "Values": ["default"]}])["SecurityGroups"][0]["GroupId"]
    return {"awsvpcConfiguration": {"subnets": subnets[:2],
            "securityGroups": [sg], "assignPublicIp": "ENABLED"}}


def one_run(label, arch, provider):
    task = ecs.run_task(
        cluster=CLUSTER, taskDefinition=f"sl107-bench-{arch.lower()}",
        capacityProviderStrategy=[{"capacityProvider": provider, "weight": 1}],
        networkConfiguration=network(), count=1)["tasks"][0]["taskArn"]
    ecs.get_waiter("tasks_stopped").wait(
        cluster=CLUSTER, tasks=[task],
        WaiterConfig={"Delay": 10, "MaxAttempts": 60})
    t = ecs.describe_tasks(cluster=CLUSTER, tasks=[task])["tasks"][0]
    tid = task.split("/")[-1]
    ops = None
    for _ in range(6):  # log delivery can lag the stop event
        try:
            ev = logs.get_log_events(logGroupName=LOG_GROUP,
                                     logStreamName=f"bench/bench/{tid}")["events"]
            for e in ev:
                if e["message"].startswith("RESULT"):
                    ops = int(e["message"].split()[1])
        except logs.exceptions.ResourceNotFoundException:
            pass
        if ops:
            break
        time.sleep(5)
    # Estimate of the billed window: image pull start to stop, 60 s minimum
    secs = max(60.0, (t["stoppedAt"] - t["pullStartedAt"]).total_seconds())
    r = RATES[arch]
    cost = secs * (r["vcpu"] * CPU / 1024 + r["gb"] * MEM_MIB / 1024)
    per_m = cost / (ops / 1e6) if ops else float("nan")
    print(f"{label:14s} exit={t['containers'][0].get('exitCode')} "
          f"ops={ops} billed~{secs:.0f}s cost=${cost:.5f} per_Mops=${per_m:.5f}")


def run():
    one_run("x86 on-demand", "X86_64", "FARGATE")
    one_run("arm on-demand", "ARM64", "FARGATE")
    one_run("x86 spot", "X86_64", "FARGATE_SPOT")
    print("Spot cost above is priced at the on-demand rate: "
          "your real Spot rate is lower, up to 70% off.")


def cleanup():
    for arch in ("x86_64", "arm64"):
        for rev in ecs.list_task_definitions(
                familyPrefix=f"sl107-bench-{arch}")["taskDefinitionArns"]:
            ecs.deregister_task_definition(taskDefinition=rev)
    ecs.delete_cluster(cluster=CLUSTER)
    logs.delete_log_group(logGroupName=LOG_GROUP)
    iam.detach_role_policy(
        RoleName=ROLE,
        PolicyArn="arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy")
    iam.delete_role(RoleName=ROLE)
    print("cleanup done")


if __name__ == "__main__":
    {"setup": setup, "run": run, "cleanup": cleanup}[sys.argv[1]]()

Run it in two steps, then verify.

python fargate_bench.py setup
python fargate_bench.py run

Verification

Each line of output should show exit=0, a non-empty ops count, and a billed window close to 60 seconds of work plus pull and startup time. The 60-second minimum applies even for a task that finishes faster. The numbers to look at are the last column. On x86 versus ARM the ops count will differ by workload and instruction mix, but the ARM cost per run is 20 percent lower by construction, so the ARM cost per million operations is lower unless your ops count falls more than 20 percent. That is the real test, and it takes ninety seconds to run on your code instead of mine. Replace the SHA loop with your service's hot path, keep the rest, and you have a migration decision backed by a number.

You can confirm the tasks ran where you expect with the CLI.

aws ecs list-tasks --cluster sl107-bench --desired-status STOPPED --region us-east-1
aws ecs describe-tasks --cluster sl107-bench --region us-east-1 \
  --tasks <task-arn> --query "tasks[0].[capacityProviderName,cpu,memory,platformVersion,stopCode]"

You should see FARGATE or FARGATE_SPOT as the capacity provider, 1024 and 2048 as size, and EssentialContainerExited as the stop code for a clean finish.

The EC2 side of the comparison

The plan for this article promised the same workload costed on EC2. The break-even is arithmetic, not a benchmark. If a Fargate task costs F per hour and an instance costs I per hour, the instance wins when it can keep at least I divided by F tasks running at once. Pull the live On-Demand price instead of trusting a table.

import json, boto3
pricing = boto3.client("pricing", region_name="us-east-1")

def ec2_hourly(instance_type):
    flt = lambda k, v: {"Type": "TERM_MATCH", "Field": k, "Value": v}
    resp = pricing.get_products(ServiceCode="AmazonEC2", Filters=[
        flt("instanceType", instance_type),
        flt("location", "US East (N. Virginia)"),
        flt("operatingSystem", "Linux"), flt("tenancy", "Shared"),
        flt("preInstalledSw", "NA"), flt("capacitystatus", "Used")])
    terms = json.loads(resp["PriceList"][0])["terms"]["OnDemand"]
    dim = next(iter(next(iter(terms.values()))["priceDimensions"].values()))
    return float(dim["pricePerUnit"]["USD"])

I = ec2_hourly("m7g.large")   # 2 vCPU, 8 GiB, ARM
F = 0.0395                    # 1 vCPU / 2 GB on Fargate ARM, from the table
print(f"instance ${I:.4f}/h; break-even = {I / F:.1f} tasks of 1 vCPU/2 GB")

Be honest with the result. An instance with 2 vCPU cannot hold more than two 1-vCPU tasks, so for 2-vCPU instances the check is simply whether the instance price is below twice the Fargate task price, and it will be. The real EC2 question is utilization: the instance bills whether the two tasks are busy, idle, or half of the instance is empty because your workload does not tile. Fargate bills exactly your tasks. EC2's price advantage is large at 80 percent utilization and disappears at 30 percent. The ECS agent and OS also reserve memory on the instance, so the usable capacity is below the label. Count what you will actually keep busy before you commit.

When it breaks

RESOURCE:CPU or "capacity is unavailable" on RunTask. You hit the 6 vCPU default quota, or the Spot pool is empty. Check Service Quotas for "Fargate On-Demand vCPU resource count" and request an increase. For Spot, the docs say Fargate does not replace Spot with On-Demand during high demand and services retry until capacity appears, so keep an On-Demand base in the strategy for anything user-facing.

CannotPullContainerError. The task could not reach the registry. A public-subnet task needs assignPublicIp ENABLED. A private-subnet task needs a NAT gateway or ECR interface endpoints plus the S3 gateway endpoint for image layers. This is the most common first failure.

ResourceInitializationError. The task could not fetch secrets, ECR credentials or create the log stream. Usually a missing route to Secrets Manager, ECR or CloudWatch Logs, or an execution role missing the managed policy.

Task stuck in PROVISIONING. Usually the subnet is out of free IPs, because every task consumes an ENI address. Check the subnet's available IPv4 count before you blame Fargate.

Exit code 137 or "Essential container exited" with out of memory. Your container went over the task memory. Fargate enforces the task size as a hard ceiling. Size up to the next valid combination, since only the fixed CPU and memory pairs are accepted.

InvalidParameterException on RegisterTaskDefinition. Almost always an invalid CPU and memory pair, or an 8, 16 or 32 vCPU task registered against a platform that does not support it. Use the table in the docs, not intuition.

Spot interruption. The task gets a SIGTERM and two minutes, the stop code is SpotInterruption. Handle SIGTERM, keep stopTimeout at or below 120 seconds (the default is 30), and make the work resumable.

Cleanup

python fargate_bench.py cleanup
aws ecs list-clusters --region us-east-1 --query "clusterArns[?contains(@, 'sl107')]"
aws logs describe-log-groups --log-group-name-prefix /sl107 --region us-east-1
aws iam get-role --role-name sl107-fargate-exec

The first command deregisters the task definitions, deletes the cluster, the log group and the role. The next three should return an empty list, an empty list, and a NoSuchEntity error. Fargate has no standing charge: once the tasks have stopped, nothing bills. If you created a NAT gateway or interface endpoints while experimenting, delete those separately, because they bill by the hour whether or not a task exists. Check Cost Explorer a day later for the Fargate usage lines to confirm only the few cents you expected.

Where to go from here

Fargate is the right default when you do not want to own capacity. The pricing is simple, per vCPU-second and GB-second, and the savings levers are equally simple: move to ARM for 20 percent, put interruptible work on Spot for up to 70, commit with a Compute Savings Plan for up to 50, and fix the network line before you tune the compute line. If Fargate's per-task isolation is not something you need and your fleet stays busy, run the arithmetic against EC2 or Managed Instances with your own utilization number. For the ECS concepts underneath (clusters, task definitions, services, rolling deploys) the previous entry in this series is SL#105. For the Kubernetes version of the same trade, SL#106 covers EKS and where its data plane starts.

Sources