0x55aa
← Back to Blog

AWS ECS Fargate: Containers Without the EC2 Babysitting ā˜ļøšŸ³

•10 min read

AWS ECS Fargate: Containers Without the EC2 Babysitting ā˜ļøšŸ³

Hot take: If you're still SSHing into EC2 instances to debug your containers, you're living in 2018 and you deserve everything that happens to you at 3 AM.

I spent two years managing a cluster of EC2 instances running Docker containers. I patched kernels. I tuned security groups. I debugged mysterious OOM kills. I woke up at 2 AM because someone accidentally ran docker system prune -a on a production node. 😭

Then I moved to ECS Fargate and I haven't thought about an EC2 instance since. Let me show you why.

What Even Is ECS Fargate? šŸ¤”

ECS (Elastic Container Service) is AWS's container orchestration platform. Think Kubernetes, but with less YAML-induced suffering.

Fargate is the launch type that removes EC2 from the picture entirely. You define how much CPU and memory your container needs. AWS figures out where to run it. You never touch a server.

WITHOUT FARGATE:
You → EC2 cluster → Docker daemon → Container
      (patch me!)   (update me!)

WITH FARGATE:
You → Container definition
AWS → Runs it somewhere magic šŸŖ„

In production, I've deployed our entire order processing microservices suite on Fargate. No EC2 instances. No patching. No "who owns this instance?" Slack messages.

The Migration That Changed Everything šŸ­

Our e-commerce backend started as Lambda functions. Great for stateless request handling. But some workloads don't fit Lambda:

  • Long-running PDF generation (Lambda's 15-minute limit was tight)
  • Background image processing with FFmpeg (deployment packages hit Lambda's limit)
  • A legacy PHP service nobody wanted to rewrite

Fargate was the answer. These needed containers, not serverless functions. But they also didn't need us to manage the infrastructure.

The basic task definition (this is really all you need to start):

{
  "family": "order-processor",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "512",
  "memory": "1024",
  "containerDefinitions": [
    {
      "name": "order-processor",
      "image": "123456789.dkr.ecr.us-east-1.amazonaws.com/order-processor:latest",
      "portMappings": [{ "containerPort": 3000 }],
      "environment": [
        { "name": "NODE_ENV", "value": "production" }
      ],
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:ssm:us-east-1:123456:parameter/prod/db-password"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/order-processor",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]
}

That secrets block is the bit that took me too long to discover. Your containers pull secrets from Parameter Store or Secrets Manager at startup — no environment variable hardcoding required. šŸ”

The Gotchas That Nearly Broke Production āš ļø

Gotcha #1: awsvpc Networking Will Bite You

Every Fargate task gets its own Elastic Network Interface (ENI). This is great for security. It's annoying for quotas.

AWS has a default limit on ENIs per VPC. I hit that limit during a traffic spike when auto-scaling spun up 50 new tasks simultaneously. Everything ground to a halt.

# Check your ENI quota BEFORE you scale
aws service-quotas get-service-quota \
  --service-code ec2 \
  --quota-code L-DF5E4CA3

Fix: Request a quota increase before you need it. Fargate scales beautifully once you're past this surprise.

Gotcha #2: Container Startup Time Isn't Zero

Fargate tasks take 10-30 seconds to start. For Lambda, cold starts are annoying at 1-2 seconds. For Fargate, that startup time means you need to plan ahead.

A serverless pattern that saved us: Don't wait for load to scale. Set your auto-scaling policy to scale early:

# Scale up aggressively (add 3 tasks when CPU > 50%, not 90%)
# Scale down conservatively (remove 1 task when CPU < 20%)

We scale up at 50% CPU utilization and cool down for 5 minutes. Yes, we're running slightly over-provisioned most of the time. The cost difference is small. The user experience difference during spikes is massive.

Gotcha #3: ECR Image Pulls Cost Money (And Time)

Every time a Fargate task starts, it pulls your Docker image from ECR. A 2GB image takes time to pull. Time costs money in Fargate (you pay per second).

When architecting on AWS, I learned: Keep your images slim. Really slim.

# BAD - full Node image is 900MB+
FROM node:18

# GOOD - Alpine base is 50MB
FROM node:18-alpine

# EVEN BETTER - multi-stage build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
CMD ["node", "server.js"]

I reduced our order-processor image from 1.4GB to 180MB. Task startup went from ~45 seconds to ~12 seconds. At scale, that compounds into serious time and cost savings.

Gotcha #4: ECS Service Connect vs Load Balancer

Getting services to talk to each other tripped me up. You have two options:

Application Load Balancer (ALB): Classic. Add an ALB in front of your service. Other services call the ALB DNS name.

ECS Service Connect: AWS's newer service mesh solution. Services register DNS names within the VPC. No ALB needed for internal traffic.

For internal service-to-service calls, Service Connect is simpler and cheaper. I'm not paying ALB hourly rates for traffic that never leaves my VPC.

"serviceConnectConfiguration": {
  "enabled": true,
  "services": [
    {
      "portName": "order-processor",
      "clientAliases": [{ "port": 3000, "dnsName": "order-processor" }]
    }
  ]
}

Now my other services call http://order-processor:3000 and ECS routes it. Beautiful.

Cost Reality Check šŸ’°

Fargate pricing: you pay for the vCPU and memory your tasks actually use, by the second.

vCPU pricing:  $0.04048 per vCPU per hour
Memory pricing: $0.004445 per GB per hour

My order-processor task:
- 0.5 vCPU Ɨ $0.04048 = $0.02024/hr
- 1 GB RAM Ɨ $0.004445 = $0.004445/hr
- Total: ~$0.025/hr per task
- 3 tasks running 24/7: $0.075/hr = ~$54/month

Compare that to the EC2 I used to run for the same workload — a t3.medium at ~$30/month, plus the hidden cost of my time patching it, securing it, babysitting it.

The real hidden cost of EC2 is the engineering hours. Fargate pays for itself when you factor in DevOps time.

Cost optimization tip: Use Fargate Spot for non-critical workloads. It's up to 70% cheaper. AWS can interrupt Spot tasks with 2-minute notice, so don't use it for your payment processor. Do use it for batch jobs, background processing, dev environments.

# In your service definition, mix On-Demand and Spot
capacityProviderStrategy:
  - capacityProvider: FARGATE
    weight: 1
    base: 1          # Always keep 1 on-demand task
  - capacityProvider: FARGATE_SPOT
    weight: 4        # 80% of additional tasks are Spot

Our background image processing runs 100% on Spot. We save ~65% versus on-demand. The occasional 2-minute interrupt is fine — jobs just retry from the queue.

The Auto-Scaling Pattern I Swear By šŸš€

ECS Fargate auto-scaling is genuinely good. Here's the pattern that's kept our e-commerce platform alive through flash sales:

# Scale on CPU (reactive)
aws application-autoscaling put-scaling-policy \
  --policy-name cpu-scaling \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration \
    "TargetValue=50.0,PredefinedMetricSpecification={PredefinedMetricType=ECSServiceAverageCPUUtilization}"

# Scale on SQS queue depth (proactive)
# When orders pile up, add more processors BEFORE CPU spikes
aws application-autoscaling put-scaling-policy \
  --policy-name queue-depth-scaling \
  --policy-type TargetTrackingScaling \
  --target-tracking-scaling-policy-configuration \
    "TargetValue=10.0,CustomizedMetricSpecification={...SQS queue depth...}"

Scaling on SQS queue depth was the insight that changed everything. Don't wait for CPU to spike — scale when work is waiting. Your tasks spin up before the CPU even notices the load.

Common Pitfalls to Avoid 🪤

Pitfall #1: Mounting EFS for shared state Don't. Fargate tasks should be stateless. If you're mounting a shared filesystem for session state, you've recreated the server you were trying to escape. Use ElastiCache or DynamoDB instead.

Pitfall #2: Forgetting task role vs execution role

  • Execution role: Used by ECS to pull images and write logs. AWS manages this mostly.
  • Task role: What YOUR code uses to call AWS services (S3, SQS, DynamoDB).

I've lost hours debugging "Access Denied" errors because I added S3 permissions to the execution role instead of the task role.

Pitfall #3: Ignoring container health checks Without health checks, ECS doesn't know your container is actually ready to serve traffic:

"healthCheck": {
  "command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
  "interval": 30,
  "timeout": 5,
  "retries": 3,
  "startPeriod": 60
}

That startPeriod is critical. Give your app 60 seconds to warm up before ECS starts judging it. Without it, ECS kills your containers before they even finish starting.

Fargate vs Lambda: When to Use Which ⚔

Use Lambda when:
āœ… Short-lived request handling (< 15 min)
āœ… Event-driven functions (S3 triggers, SQS consumers)
āœ… Infrequent invocations (pay only when called)
āœ… Stateless, single-purpose functions

Use Fargate when:
āœ… Long-running processes (PDF generation, video processing)
āœ… Services needing persistent connections (WebSockets)
āœ… Legacy apps that can't be easily refactored
āœ… Steady, continuous traffic (Fargate is cheaper than Lambda at scale)
āœ… Containers that need more than 10GB RAM

The dirty secret: At very high traffic, Fargate is cheaper than Lambda per-request. Lambda's compute efficiency is higher for spiky, infrequent workloads. Fargate wins for always-on, high-throughput services. Do the math for your specific workload.

TL;DR — Your Fargate Quick-Start Checklist āœ…

  1. Slim your Docker images — multi-stage builds, Alpine base images, no dev dependencies
  2. Set up awslogs logging — you need CloudWatch logs from day one
  3. Use SSM Parameter Store for secrets — not environment variables in the task definition
  4. Set early auto-scaling thresholds — scale at 50% CPU, not 90%
  5. Request ENI quota increases before you need them
  6. Use Fargate Spot for batch/background jobs — 65%+ cost savings
  7. Implement proper health checks with generous startPeriod
  8. Use ECS Service Connect for internal service-to-service calls
  9. Scale on queue depth, not just CPU — proactive beats reactive

The Bottom Line šŸ’”

When architecting on AWS, I learned that the best infrastructure is the infrastructure you don't think about. Fargate got me there.

Before Fargate: three engineers managing EC2 clusters, monthly patching windows, "who owns this instance" archaeology, 3 AM "the host is down" pagerduty alerts.

After Fargate: define what your container needs, push to ECR, let AWS handle the rest. Scale automatically. Pay per second. Sleep through the night.

Yes, Lambda is still my first choice for most workloads. But when Lambda doesn't fit, Fargate is the answer — not a fleet of EC2 instances I have to babysit for the next three years.


Running containers on AWS? Find me on LinkedIn — I'm always happy to nerd out about container architecture.

Want production-ready Fargate templates? Check my GitHub for ECS task definitions and Terraform modules I actually use.

Now go containerize something and let AWS figure out where to run it. šŸ³šŸš€


P.S. The moment you set up Fargate Spot for your batch jobs and see 65% savings on your AWS bill, you will feel a joy that is difficult to describe. Pure engineering satisfaction. šŸ’ø

P.P.S. If you're coming from Kubernetes, ECS will feel weirdly simple. That's not a bug. Embrace it.